默認shiro鑑權是基於session認證,也能實現無狀態Web。項目改造成前後端分離時,在原有的session認證下擴展出一套無狀態認證。將問題分離成以下幾點
1、攔截無狀態請求
2、實現多realm共存 一個realm處理原先的session認證 一個處理無狀態認證
3、開啓原先的session管理 禁用無狀態請求的session管理 否則 一個瀏覽器同時存在兩種請求時會。。。
建一個過濾器攔截api請求
重寫 isAccessAllowed onAccessDenied
onAccessDenied || onAccessDenied返回true表示通過
/**
* 區分session和無狀態請求
* @author bbq
* @version 2019-12-30
*/
@Service
public class StatelessAuthenticationFilter extends org.apache.shiro.web.filter.AccessControlFilter {
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
StatelessToken statelessToken = new StatelessToken();
/**
業務代碼 拿到你的token 填充到statelessToken
String token =。。。
*/
statelessToken.setToken(token);
getSubject(servletRequest, servletResponse).login(statelessToken);
return true;
}
}
StatelessToken
用於區分調用的realm
public class StatelessToken implements org.apache.shiro.authc.AuthenticationToken{
private String token;
private int expire;
private User user;
}
shiro.xml
攔截 api 接口 用 StatelessAuthenticationFilter 過濾器
<bean id="statelessAuthenticationFilter" class="路徑.Stateless.StatelessAuthenticationFilter"></bean>
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<property name="filters">
<map>
<entry key="statelessAuthenticationFilter" value-ref="statelessAuthenticationFilter"/>
<entry key="authc" value-ref="formAuthenticationFilter"/>
</map>
</property>
<property name="filterChainDefinitions">
<ref bean="shiroFilterChainDefinitions"/>
</property>
</bean>
<bean name="shiroFilterChainDefinitions" class="java.lang.String">
<constructor-arg>
<value>
${BasePath}/**/api/** = statelessAuthenticationFilter
</value>
</constructor-arg>
</bean>
建一個realm用於處理無狀態請求
重寫supports可以通過token的不同區分是不是來自無狀態請求
注意:
shiro在認證和授權時會分別調用realm
由於supports參數是AuthenticationToken 授權參數是 PrincipalCollection
所以授權通過Principal區分
/**
* 區分session和無狀態請求
* @author bbq
* @version 2019-12-30
*/
@Service
public class StatelessAuthorizingRealm extends org.apache.shiro.realm.AuthorizingRealm {
@Override
public boolean supports(AuthenticationToken token) {
//根據realm綁定的token不同,登錄時自動調用相關realm進行登錄
return token instanceof StatelessToken;
}
/**
* 認證回調函數, 登錄時調用
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) {
StatelessToken token = (StatelessToken) authcToken;
/**
業務代碼 身份認證
*/
}
//重寫 不用shiro自帶的認證 爲空即可
@Override
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
}
/**
* 授權查詢回調函數, 進行鑑權但緩存中無用戶的授權信息時調用
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
if(!(getAvailablePrincipal(principals) instanceof Principal)){
return null;
}
/**
業務代碼 鑑權
Principal principal = (Principal) getAvailablePrincipal(principals);
獲取你的用戶 查詢權限 填充進 SimpleAuthorizationInfo
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermission(permission);
*/
}
/**
重寫一些鑑權方法 之前怎麼搞 這裏就怎麼搞
*/
/**
* 授權用戶信息
*/
public static class Principal implements Serializable {
private static final long serialVersionUID = 1L;
private String id; // 編號
private String token;
private User user = null;
public Principal(User user, String token) {
this.id = user.getId();
this.user = user;
}
public String getId() {
return id;
}
public User getToken() {
return user;
}
@Override
public String toString() {
return id;
}
}
}
shiro.xml
<bean id="statelessAuthorizingRealm" class="路徑.Stateless.StatelessAuthorizingRealm">
<property name="authenticationCachingEnabled" value="false"/>
</bean>
<!-- 定義Shiro安全管理配置 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="authenticator" ref="multiRealmAuthenticator"/>
<property name="realms">
<list>
<ref bean="systemAuthorizingRealm"/>
<ref bean="statelessAuthorizingRealm"/>
</list>
</property>
<property name="sessionManager" ref="sessionManager" />
<property name="cacheManager" ref="shiroCacheManager" />
</bean>
處理多realm共存鑑權失敗只會拋出 沒有匹配realm的大異常
重寫shiro的ModularRealmAuthenticator 當單個realm中有異常時就直接拋出
注意:原先的realm也要通過相匹配的token進行選擇
public class MultiRealmAuthenticator extends org.apache.shiro.authc.pam.ModularRealmAuthenticator {
private static final Logger log = LoggerFactory.getLogger(ModularRealmAuthenticator.class);
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) throws AuthenticationException {
AuthenticationStrategy strategy = this.getAuthenticationStrategy();
AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
if (log.isTraceEnabled()) {
log.trace("Iterating through {} realms for PAM authentication", realms.size());
}
AuthenticationException authenticationException = null;
Iterator i$ = realms.iterator();
while(i$.hasNext()) {
Realm realm = (Realm)i$.next();
aggregate = strategy.beforeAttempt(realm, token, aggregate);
if (realm.supports(token)) {
log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm);
AuthenticationInfo info = null;
Throwable t = null;
try {
info = realm.getAuthenticationInfo(token);
} catch (AuthenticationException var11) {
authenticationException = var11;
t = var11;
if (log.isDebugEnabled()) {
String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:";
log.debug(msg, var11);
}
}
aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
} else {
log.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token);
}
}
if(authenticationException != null){
throw authenticationException;
}
aggregate = strategy.afterAllAttempts(token, aggregate);
return aggregate;
}
}
shiro.xml
<bean id="multiRealmAuthenticator" class="com.thinkgem.jeesite.modules.sys.security.Stateless.MultiRealmAuthenticator">
<property name="authenticationStrategy">
<!-- 認證策略 -->
<!-- <bean class="org.apache.shiro.authc.pam.AllSuccessfulStrategy"></bean> -->
<bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy"></bean>
</property>
</bean>
session管理可以參考下文
https://blog.csdn.net/qq_39412825/article/details/88365080
最後
session與jwt的不同:session認證是保險箱在服務器,密碼在用戶手中,用戶把密碼送到服務器解開自己的保險箱,而jwt則是保險箱放在用戶手中,服務器什麼都不放,當用戶把保險箱送來,服務器摸一摸保險箱,敲打敲打,認爲保險箱是自己家生產的就打開它。
這樣當服務器開分號時,採用session方式就只能幫用戶解鎖在自己分號的保險箱,用戶如果讓a分號打開存在b分號的保險箱,就得順豐快遞從a送到b送過來。而jwt方式每一家分號都能打開任意用戶的保險箱。
由於項目session加入了redis緩存,也可作爲分佈式使用(開啓順豐服務),而 token的生成只是加入一段隨機串加密保存在redis中(分號老闆認不出自家的保險箱,而是把保險箱的生產編碼記在賬本上,每家分號只記錄自己家賣出去的保險箱),這樣就離不開與服務器進行交互,所以這裏的token本質和sessionid一樣,並沒有實現服務器無關性,所以最後沒有采用多realm認證,而是將sessionid賦予token參數,api請求也複用session認證的方式。
複用session實現前後端分離無cookie的api鑑權
看另一篇文
shiro複用session實現前後端分離鑑權