Shiro 核心 API
Subject:用戶主體(每次請求都會創建Subject)。
principal:代表身份。可以是用戶名、郵件、手機號碼等等,用來標識一個登錄主體的身份。
credential:代表憑證。常見的有密碼,數字證書等。
SecurityManager:安全管理器(關聯 Realm),用於安全校驗。
Realm:Shiro 連接數據的橋梁。
Cryptography:加密,保護數據的安全性,如密碼加密存儲到數據庫,而不是明文存儲。
Caching:緩存,比如用戶登錄后,其用戶信息、擁有的角色/權限不必每次去查,這樣可以提高效率。
CacheManager:緩存控制器,來管理如用戶、角色、權限等緩存的控制器。
SessionManager:會話管理,即用戶登錄后就是一次會話,在沒有退出之前,它的所有信息都在會話中。
SessionDAO:會話儲存。
Shiro 認證與授權
身份認證:
Step1:應用程序代碼調用 Subject.login(token) 方法后,傳入代表最終用戶身份的 AuthenticationToken 實例 Token。
Step2:將 Subject 實例委托給應用程序的 SecurityManager(Shiro 的安全管理)並開始實際的認證工作。
Step3、4、5:SecurityManager 根據具體的 Realm 進行安全認證。
權限認證(涉及到三張表:用戶表、角色表和權限表):
權限(Permission):即操作資源的權利(添加、修改、刪除、查看操作的權利)。
角色(Role):指的是用戶擔任的角色,一個角色可以有多個權限。
用戶(User):在 Shiro 中,代表訪問系統的用戶,即上面提到的 Subject 認證主體。
請求步驟:
瀏覽器發出第一次請求的時候,去redis里找不到對應的session,會進入到登錄頁。
當在登錄頁輸入完正確的賬號密碼后,才能登錄成功。
根據登錄成功后的session生成sessionId,並傳到前端瀏覽器中,瀏覽器以cookie存儲,同時將session存儲到redis中。
每次瀏覽器訪問后台,都會刷新session的過期時間expireTime。
當瀏覽器再次請求時,將當前瀏覽器中的所有的cookie設置到request headers請求頭中。
根據傳入的sessionId串到共享的redis存儲中匹配。
如果匹配不到,則會跳轉到登錄頁,如果匹配成功,則會訪問通過。
Spring Boot Shiro 依賴
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
</dependency>
自定義 Realm
自定義 Realm 需要繼承 AuthorizingRealm 類,該類封裝了很多方法,且繼承自 Realm 類。
重寫以下兩個方法:
doGetAuthenticationInfo() 方法:用來驗證當前登錄的用戶,獲取認證信息。
doGetAuthorizationInfo() 方法:為當前登錄成功的用戶授予權限和分配角色。
public class CustomRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/**
* 登錄成功的用戶授予權限和分配角色
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//獲取用戶名
String account = (String) principals.getPrimaryPrincipal();
//從數據庫查詢用戶角色信息
User user = userService.getUserByAccount(account);
//設置角色
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
Set<String> roles = new HashSet();
if (user.getAdmin()) {
roles.add(Base.ROLE_ADMIN);
}
authorizationInfo.setRoles(roles);
return authorizationInfo;
}
/**
* 執行認證邏輯
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//獲取用戶名
String account = (String) token.getPrincipal();
//從數據庫查詢該用戶
User user = userService.getUserByAccount(account);
if (null == user) {
throw new UnknownAccountException(); //沒找到該帳號
}
if (UserStatus.blocked.equals(user.getStatus())) {
throw new LockedAccountException(); //帳號鎖定
}
//傳入用戶名和密碼進行身份認證,並返回認證信息
return new SimpleAuthenticationInfo(
user.getAccount(),
user.getPassword(),
ByteSource.Util.bytes(user.getSalt()), //鹽(密碼加鹽加密處理)
getName()
);
}
}
自定義 SessionDAO
Cachemanager緩存里可以包含權限認證的緩存、用戶及權限信息的緩存等,也可以做Session緩存。
SessionDAO是做Session持久化的,可以使用Redis來存儲。
默認 SessionDAO:MemorySessionDAO:
將Session保存在內存中,存儲結構是ConcurrentHashMap。
public class MemorySessionDAO extends AbstractSessionDAO {
private ConcurrentMap<Serializable, Session> sessions = new ConcurrentHashMap();
protected Serializable doCreate(Session session) {
Serializable sessionId = this.generateSessionId(session);
this.assignSessionId(session, sessionId);
this.storeSession(sessionId, session);
return sessionId;
}
}
/**
* 將Session保存到Redis
*/
public class CustomSessionDAO extends CachingSessionDAO {
@Autowired
private RedisTemplate redisTemplate;
//默認緩存過期時間:30分鍾
public final static long DEFAULT_EXPIRE = 60 * 30;
@Override
protected Serializable doCreate(Session session) {
//創造SessionId
Serializable sessionId = generateSessionId(session);
//注冊SessionId
assignSessionId(session, sessionId);
//緩存Session
redisTemplate.opsForValue().set(sessionId.toString(), session, DEFAULT_EXPIRE, TimeUnit.SECONDS);
return sessionId;
}
@Override
protected void doUpdate(Session session) {
if (session instanceof ValidatingSession && !((ValidatingSession) session).isValid()) {
//會話過期/停止
return;
}
redisTemplate.opsForValue().set(session.getId().toString(), session, DEFAULT_EXPIRE, TimeUnit.SECONDS);
}
@Override
protected void doDelete(Session session) {
redisTemplate.delete(session.getId().toString());
}
@Override
protected Session doReadSession(Serializable sessionId) {
return (Session) redisTemplate.opsForValue().get(sessionId.toString());
}
}
自定義 SessionManager(待完善)
public class CustomSessionManager extends DefaultWebSessionManager {
public static final String TOKEN = "token";
/**
* 調用登陸接口的時候,是沒有token的。
* 登陸成功后,產生了token,我們把它放到request中。
* 返回結果給客戶端的時候,把它從request中取出來,並且傳遞給客戶端。
* 客戶端每次帶着這個token過來,就相當於是瀏覽器的cookie的作用,也就能維護會話了。
*/
@Override
public Serializable getSessionId(SessionKey key) {
Serializable sessionId = key.getSessionId();
if(sessionId == null && WebUtils.isWeb(key)){
HttpServletRequest request = WebUtils.getHttpRequest(key);
HttpServletResponse response = WebUtils.getHttpResponse(key);
sessionId = this.getSessionId(request,response);
}
HttpServletRequest request = WebUtils.getHttpRequest(key);
request.setAttribute(TOKEN,sessionId.toString());
return sessionId;
}
/**
* DefaultWebSessionManager默認實現中,是通過Cookie確定SessionId。
* 重寫時,只需要把獲取SessionId的方式變更為在request header中獲取即可。
*/
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String id = httpRequest.getHeader(TOKEN);
if (!StringUtils.isEmpty(id)) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
}
return super.getSessionId(request, response);
}
}
配置類 ShiroConfig
@Configuration
public class ShiroConfig {
/**
* 創建 ShiroFilterFactoryBean
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//設置securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
//LinkedHashMap 是有序的,進行順序攔截器配置
Map<String, String> filterMap = new LinkedHashMap();
filterMap.put("/static/**", "anon"); //無需認證可以訪問
filterMap.put("/login", "anon");
filterMap.put("/register", "anon");
//配置退出過濾器,其中具體的退出代碼Shiro已經替我們實現了,登出后跳轉配置的LoginUrl
filterMap.put("/logout", "logout");
filterMap.put("/**/create", "authc"); //必須認證才可以訪問
filterMap.put("/**/update", "authc");
filterMap.put("/**/delete", "authc");
filterMap.put("/upload", "authc");
filterMap.put("/admin", "perms[admin]"); //資源必須得到資源權限才能訪問,多個參數寫法:perms["admin,user"]
filterMap.put("/admin", "role[admin]"); //資源必須得到角色權限才能訪問
filterMap.put("/**", "anon");
//設置默認登錄的URL,身份認證失敗會訪問該URL
shiroFilterFactoryBean.setLoginUrl("/login");
//身份認證設置成功之后要跳轉的URL
shiroFilterFactoryBean.setSuccessUrl("/index");
//設置未授權界面,權限認證失敗會訪問該URL
shiroFilterFactoryBean.setUnauthorizedUrl("/unAuthorized");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
/**
* 配置安全管理器:DefaultWebSecurityManager
*/
@Bean
public SecurityManager securityManager(CustomRealm realm, SessionManager sessionManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
securityManager.setSessionManager(sessionManager);
return securityManager;
}
/**
* 創建自定義 Realm
*/
@Bean
public CustomRealm customRealm() {
CustomRealm shiroRealm = new CustomRealm();
//配置密碼加密
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
matcher.setHashAlgorithmName("md5"); //加密方式
matcher.setHashIterations(2); //加密次數
shiroRealm.setCredentialsMatcher(matcher);
return shiroRealm;
}
/**
* 配置 SessionManager
*/
@Bean
public SessionManager sessionManager() {
CustomSessionManager customSessionManager = new CustomSessionManager();
//session過期時間:1小時(默認半小時)
customSessionManager.setGlobalSessionTimeout(60 * 60 * 1000);
customSessionManager.setSessionDAO(new CustomSessionDAO());
return customSessionManager;
}
/**
* Spring Boot Shiro 開啟注釋
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
Session的查詢、刷新
SimpleSession幾個屬性:
Serializable id:session id;
Date startTimestamp:session的創建時間;
Date stopTimestamp:session的失效時間;
Date lastAccessTime:session的最近一次訪問時間,初始值是startTimestamp
long timeout:session的有效時長,默認30分鍾
boolean expired:session是否到期
Map<Object, Object> attributes:session的屬性容器
查詢:Session session = SecurityUtils.getSubject().getSession(); //返回的就是綁定在當前subjuct的session。
刷新:SimpleSession的touch()
public void touch() {
this.lastAccessTime = new Date();
}
Web應用,每次進入ShiroFilter都會自動調用session.touch()來更新最后訪問時間。
過期時間判斷:當前時間-lastAccessTime=是否超過有效時長。
Shiro支持三種方式的授權
1、編程式,通過寫if/else授權代碼塊
Subject subject = SecurityUtils.getSubject();
if(subject.hasRole("admin")) {
// 有權限,執行相關業務
} else {
// 無權限,給相關提示
}
2、注解式,通過在執行的Java方法上放置相應的注解完成
@RequiresPermissions("admin")
public List<User> listUser() {
// 有權限,獲取數據
}
3、JSP/GSP標簽,在JSP/GSP頁面通過相應的標簽完成
<shiro:hasRole name="admin">
<!-- 有權限 -->
</shiro:hasRole>
Spring Boot 集成 Shiro 和 Ehcache(待完善)
引入配置文件 ehcache.xml:
application.xml配置文件添加:spring.cache.ehcache.config=classpath:ehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<diskStore path="java.io.tmpdir/shiro-cache"/>
<defaultCache //默認緩存策略
eternal="false" //對象是否永久有效,一但設置了,timeout將不起作用
maxElementsInMemory="1000" //緩存最大元素數目
overflowToDisk="false" //當內存中對象數量達到maxElementsInMemory時,Ehcache將對象寫到磁盤中。
diskPersistent="false" //是否在磁盤上持久化。指重啟JVM后,數據是否有效。默認為false
timeToIdleSeconds="0" //設置對象在失效前的允許閑置時間(單位:s),默認是0(永久有效)。
timeToLiveSeconds="600" //設置對象在失效前允許存活時間(單位:s),默認是0(永久有效)。
//當達到maxElementsInMemory限制時,Ehcache將會根據指定的策略去清理內存
//LRU(最近最少使用,默認策略)、FIFO(先進先出)、LFU(最少訪問次數)
memoryStoreEvictionPolicy="LRU" />
<cache
name="users" //緩存名稱
eternal="false"
maxElementsInMemory="500"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="0"
timeToLiveSeconds="300"
memoryStoreEvictionPolicy="LRU" />
</ehcache>
Controller 與 Service 使用
Controller:
@PostMapping("/login")
public Result login(@RequestBody User user) {
Result r = new Result();
//獲取Subject用戶主體
Subject subject = SecurityUtils.getSubject();
//封裝用戶數據
UsernamePasswordToken token = new UsernamePasswordToken(user.getAccount(), user.getPassword());
try {
//執行認證操作(調用UserRealm中的方法認證)
subject.login(token);
//認證通過
User currentUser = userService.getUserByAccount(user.getAccount());
subject.getSession().setAttribute(Base.CURRENT_USER, currentUser);
r.setResultCode(ResultCode.SUCCESS);
r.getData().put("token", subject.getSession().getId());
} catch (UnknownAccountException e) {
r.setResultCode(ResultCode.USER_NOT_EXIST); //用戶不存在
} catch (LockedAccountException e) {
r.setResultCode(ResultCode.USER_ACCOUNT_FORBIDDEN); //賬號被鎖定
}catch (IncorrectCredentialsException e) {
r.setResultCode(ResultCode.USER_LOGIN_PASSWORD_ERROR); //密碼錯誤
} catch (AuthenticationException e) {
r.setResultCode(ResultCode.USER_LOGIN_ERROR); //認證錯誤(包含以上錯誤)
}
return r;
}
@PostMapping("/register")
public Result register(@RequestBody User user) {
Result r = new Result();
User temp = userService.getUserByAccount(user.getAccount());
if (null != temp) {
r.setResultCode(ResultCode.USER_HAS_EXISTED);
return r;
}
userService.saveUser(user);
r.setResultCode(ResultCode.SUCCESS);
return r;
}
Service:
@Override
@Transactional
public void saveUser(User user) {
//密碼加密
String newPassword = new SimpleHash(
"md5",user.getPassword(),
ByteSource.Util.bytes("salt"),2).toHex();
user.setPassword(newPassword);
return userRepository.save(user);
}