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实现前后端分离鉴权

原创文章 6 获赞 18 访问量 3168

猜你喜欢

转载自blog.csdn.net/qq_24054301/article/details/103833855
今日推荐