Shiro實現session和無狀態token認證共存

默認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實現前後端分離鑑權

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章