默认shiro鉴权是基于session认证,也能实现无状态Web。项目改造成前后端分离时,在原有的session认证下扩展出一套无状态认证。将问题分离成以下几点
1、拦截无状态请求
2、实现多realm共存 一个realm处理原先的session认证 一个处理无状态认证
3、开启原先的session管理 禁用无状态请求的session管理 否则 一个浏览器同时存在两种请求时会。。。
建一个过滤器拦截api请求
重写 isAccessAllowed onAccessDenied
onAccessDenied || onAccessDenied返回true表示通过
/**
* 区分session和无状态请求
* @author bbq
*/
@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
*/
@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管理可以参考下文
最后
session与jwt的不同:session认证是保险箱在服务器,密码在用户手中,用户把密码送到服务器解开自己的保险箱,而jwt则是保险箱放在用户手中,服务器什么都不放,当用户把保险箱送来,服务器摸一摸保险箱,敲打敲打,认为保险箱是自己家生产的就打开它。
这样当服务器开分号时,采用session方式就只能帮用户解锁在自己分号的保险箱,用户如果让a分号打开存在b分号的保险箱,就得顺丰快递从a送到b送过来。而jwt方式每一家分号都能打开任意用户的保险箱。
由于项目session加入了redis缓存,也可作为分布式使用(开启顺丰服务),而 token的生成只是加入一段随机串加密保存在redis中(分号老板认不出自家的保险箱,而是把保险箱的生产编码记在账本上,每家分号只记录自己家卖出去的保险箱),这样就离不开与服务器进行交互,所以这里的token本质和sessionid一样,并没有实现服务器无关性,所以最后没有采用多realm认证,而是将sessionid赋予token参数,api请求也复用session认证的方式。