基础的什么配置这些都不说了,百度一下什么都有,直接上干货。
Shiro切入点是从web.xml文件,通过filter进行拦截。
直接看DelegatingFilterProxy这个类,很简单,父类就是一个filter,肯定会初始化filter,后面会调用这个方法:
@Override
protected void initFilterBean() throws ServletException {
synchronized (this.delegateMonitor) {
if (this.delegate == null) {
// If no target bean name specified, use filter name.
if (this.targetBeanName == null) {
this.targetBeanName = getFilterName();
}
// Fetch Spring root application context and initialize the delegate early,
// if possible. If the root application context will be started after this
// filter proxy, we'll have to resort to lazy initialization.
WebApplicationContext wac = findWebApplicationContext();
if (wac != null) {
this.delegate = initDelegate(wac);
}
}
}
}
这个方法主要的作用就是获取filterName,通过filterName和类型从spring Bean容器中获取Bean然后赋予delegate。filtetName很显然是shiroFilter,然后这个类型就是Filter.class。
至于想要获取的这个Bean,也很简单。直接找spring中shiro的这块配置。
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="${shiro.loginUrl}"/>
<!--登录成功默认跳转页面,不配置则跳转至”/”。 如果登陆前点击的一个需要登录的页面,则在登录自动跳转到那个需要登录的页面。不跳转到此。 -->
<property name="successUrl" value="${shiro.successUrl}"/>
<!--没有权限跳转的链接 -->
<property name="unauthorizedUrl" value="/login"/>
<property name="filterChainDefinitions">
<value>
/favicon.ico =anon
/css/** = anon
/js/** = anon
/druid/** = anon
/ecoupon/info/detail = anon
/** = authc
</value>
</property>
</bean>
<!-- 权限管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!-- 基于redis登录校验的实现 -->
<property name="realm" ref="systemAuthorizingRealm"/>
<!-- session 管理器 -->
<property name="sessionManager" ref="sessionManager"/>
</bean>
ShiroFilterFactoryBean这个类实现FactoryBean,故在getBean时会执行
public Object getObject() throws Exception {
if (instance == null) {
instance = createInstance();
}
return instance;
}
protected AbstractShiroFilter createInstance() throws Exception {
log.debug("Creating Shiro Filter instance.");
SecurityManager securityManager = getSecurityManager();
if (securityManager == null) {
String msg = "SecurityManager property must be set.";
throw new BeanInitializationException(msg);
}
if (!(securityManager instanceof WebSecurityManager)) {
String msg = "The security manager does not implement the WebSecurityManager interface.";
throw new BeanInitializationException(msg);
}
FilterChainManager manager = createFilterChainManager();
//Expose the constructed FilterChainManager by first wrapping it in a
// FilterChainResolver implementation. The AbstractShiroFilter implementations
// do not know about FilterChainManagers - only resolvers:
PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
chainResolver.setFilterChainManager(manager);
//Now create a concrete ShiroFilter instance and apply the acquired SecurityManager and built
//FilterChainResolver. It doesn't matter that the instance is an anonymous inner class
//here - we're just using it because it is a concrete AbstractShiroFilter instance that accepts
//injection of the SecurityManager and FilterChainResolver:
return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
}
所以最后肯定就会获取到SpringShiroFilter对应的bean。其实就是最后new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver)产生的对象,
这样整个入口算是清楚了。
比如现在正好有一个请求过来了,第一步,肯定是DelegatingFilterProxy中进行doFilter拦截住。
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// Lazily initialize the delegate if necessary.
Filter delegateToUse = this.delegate;
if (delegateToUse == null) {
synchronized (this.delegateMonitor) {
if (this.delegate == null) {
WebApplicationContext wac = findWebApplicationContext();
if (wac == null) {
throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener registered?");
}
this.delegate = initDelegate(wac);
}
delegateToUse = this.delegate;
}
}
// Let the delegate perform the actual doFilter operation.
invokeDelegate(delegateToUse, request, response, filterChain);
}
delegateToUse这个我们也获取到了,后面最重要的是invokeDelegate(delegateToUse, request, response, filterChain)这个方法。
后面会调用OncePerRequestFilter中的doFilter方法,很简单,这个是SpringShiroFilter的父类,然后就会执行doFilterInternal的实现,这个实现在AbstractShiroFilter
中。重点来了,
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
throws ServletException, IOException {
Throwable t = null;
try {
这里就是对serverRequest和serverResponse包装
final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
这个才是我们的重头戏
final Subject subject = createSubject(request, response);
//noinspection unchecked
这个会执行SubjectCallAble中的call方法将生成的subject对象绑定到ThreadContext中,这个原理很简单,就是ThreadLocal对当前线程进行绑定。
subject.execute(new Callable() {
public Object call() throws Exception {
绑定到当前线程之后,更新session最后获取时间。
updateSessionLastAccessTime(request, response);
executeChain(request, response, chain);
return null;
}
});
} catch (ExecutionException ex) {
t = ex.getCause();
} catch (Throwable throwable) {
t = throwable;
}
if (t != null) {
if (t instanceof ServletException) {
throw (ServletException) t;
}
if (t instanceof IOException) {
throw (IOException) t;
}
//otherwise it's not one of the two exceptions expected by the filter method signature - wrap it in one:
String msg = "Filtered request failed.";
throw new ServletException(msg, t);
}
}
后面会进入这个创建Subject的方法,看看具体干了什么
public Subject createSubject(SubjectContext subjectContext) {
//create a copy so we don't modify the argument's backing map:
很简单,这个就是相当于包装,把subjectContext包装进去。
SubjectContext context = copy(subjectContext);
把secutityManager放进去。
context = ensureSecurityManager(context);
获取服务器session
context = resolveSession(context);
这个其实没啥用,使用认证信息和权限信息缓存时才会有用,这个在多节点的服务来说,基本不会在服务器做这个,相当于把一个Map的key放进context中,第一次进来,肯定是空。
等到认证完成后,才会有值,是从session中获取的。
context = resolvePrincipals(context);
这个很重要,就是将认证状态、host、request、response等信息放到subject中,
Subject subject = doCreateSubject(context); save(subject); return subject; }
// 将认证状态和上面说的那个key放到session中,刚开始肯定什么都不做,都是空。登陆后才会真正在session中写入。
save(subject);
return subject;
以上Filter走完后,中间其他过程不要理会,后面进行登陆操作。
通过SecurityUtils.getSubject()获取当前线程中的subject,也就是上面filter中创建的subject。
调用subject.login()方法进行登陆认证。
public void login(AuthenticationToken token) throws AuthenticationException {
clearRunAsIdentitiesInternal();
获取登陆中的subject,解释在下面方法中。
Subject subject = securityManager.login(this, token);
PrincipalCollection principals;
String host = null;
if (subject instanceof DelegatingSubject) {
DelegatingSubject delegating = (DelegatingSubject) subject;
//we have to do this in case there are assumed identities - we don't want to lose the 'real' principals:
principals = delegating.principals;
host = delegating.host;
} else {
principals = subject.getPrincipals();
}
if (principals == null || principals.isEmpty()) {
String msg = "Principals returned from securityManager.login( token ) returned a null or " +
"empty value. This value must be non null and populated with one or more elements.";
throw new IllegalStateException(msg);
}
this.principals = principals;
this.authenticated = true;
if (token instanceof HostAuthenticationToken) {
host = ((HostAuthenticationToken) token).getHost();
}
if (host != null) {
this.host = host;
}
// 从登陆中的subject中获取session,并将session放入filter中生成的subject。
Session session = subject.getSession(false);
if (session != null) {
this.session = decorate(session);
} else {
this.session = null;
}
}
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
// 在自己实现的Realm中进行认证并获取认证信息
info = authenticate(token);
} catch (AuthenticationException ae) {
try {
onFailedLogin(token, ae, subject);
} catch (Exception e) {
if (log.isInfoEnabled()) {
log.info("onFailedLogin method threw an " +
"exception. Logging and propagating original AuthenticationException.", e);
}
}
throw ae; //propagate
}
将token和info还有以前的subject来创建登陆中的subject,这是一个新的subject,此处会在服务端生成一个session,并将一些信息返回给写入
如:认证状态和上面说的那个无用的key。
Subject loggedIn = createSubject(token, info, subject);
onSuccessfulLogin(token, info, loggedIn);
return loggedIn;
}
以上shiro登陆流程算是走完了,如果使用默认的sessionManager,也就是ServletContainerSessionManager会在会话后在cookie生成一个JsessionId
用户在第二次进来的时候服务器会根据JseeionId帮你找到服务器的上次登陆的session,里面包含登陆的信息。
save(subject);也就是这个方法里面做的。最简单的shiro登陆流程算是结束了。
但是这种在服务器存放的session有很多问题,在多节点的项目中,必然是登陆功能要重新登陆好几次,原因很简单,session不共享。
为了解决这个问题,可以不要使用默认的sessionManager(如果没有配置就是默认的ServletContainerSessionManager)。
配置使用DefaultWebSessionManager,然后在内部配置实现。
<!-- 权限管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!-- 基于redis登录校验的实现 -->
<property name="realm" ref="systemAuthorizingRealm"/>
<!-- session 管理器 -->
<property name="sessionManager" ref="sessionManager"/>
</bean>
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<!--session 超时时间:30分钟 -->
<property name="globalSessionTimeout" value="${shiro.sessionTimeout}"/>
<property name="sessionIdCookie" ref="sessionIdCookie"/>
<!--持久化shiro session,以适应集群环境-->
<property name="sessionDAO" ref="redisSessionDao"/>
</bean>
<!-- 指定本系统SESSIONID, 默认为: JSESSIONID 问题: 与SERVLET容器名冲突, 如JETTY, TOMCAT
等默认JSESSIONID, 当跳出SHIRO SERVLET时如ERROR-PAGE容器会为JSESSIONID重新分配值导致登录会话丢失! -->
<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="${shiro.sid}"/>
<property name="httpOnly" value="true"/>
<property name="domain" value="${shiro.domain}"/>
</bean>
<bean id="redisSessionDao" class="com.***.***.shiro.RedisSessionDAO">
<property name="expire" value="2700"/>
<property name="keyPrefix" value="${shiro.sessionkey}"/>
</bean>
使用redis作为存储session的容器就可以完美的解决了,另外cookie也完全可以自定义,不必完全是JsessionId。
中间过程很简单,就不一一叙述了。
protected void mergePrincipals(Subject subject) {
//merge PrincipalCollection state:
PrincipalCollection currentPrincipals = null;
//SHIRO-380: added if/else block - need to retain original (source) principals
//This technique (reflection) is only temporary - a proper long term solution needs to be found,
//but this technique allowed an immediate fix that is API point-version forwards and backwards compatible
//
//A more comprehensive review / cleaning of runAs should be performed for Shiro 1.3 / 2.0 +
if (subject.isRunAs() && subject instanceof DelegatingSubject) {
try {
Field field = DelegatingSubject.class.getDeclaredField("principals");
field.setAccessible(true);
currentPrincipals = (PrincipalCollection)field.get(subject);
} catch (Exception e) {
throw new IllegalStateException("Unable to access DelegatingSubject principals property.", e);
}
}
if (currentPrincipals == null || currentPrincipals.isEmpty()) {
currentPrincipals = subject.getPrincipals();
}
获取session,获取不到不会创建
Session session = subject.getSession(false);
if (session == null) {
if (!CollectionUtils.isEmpty(currentPrincipals)) {
获取session,获取不到会自己创建,这个在shiro中实现的居然直接是uuid,有点随意啊。
session = subject.getSession();
session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);
}
//otherwise no session and no principals - nothing to save
} else {
PrincipalCollection existingPrincipals =
(PrincipalCollection) session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
if (CollectionUtils.isEmpty(currentPrincipals)) {
if (!CollectionUtils.isEmpty(existingPrincipals)) {
session.removeAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
}
//otherwise both are null or empty - no need to update the session
} else {
if (!currentPrincipals.equals(existingPrincipals)) {
session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);
}
//otherwise they're the same - no need to update the session
}
}
}
在这个getSession方法中会对根据不同的sessionManager获取sessionId。
redis的sessionDAO实现也发出来吧,都是可以直接用的。
public class RedisSessionDAO extends AbstractSessionDAO {
private static Logger logger = LoggerFactory.getLogger(RedisSessionDAO.class);
/**
* The Redis key prefix for the sessions
*/
private String keyPrefix = "shiro_redis_session:";
/**
* redis 缓存过期时间/秒
*/
private int expire = 60 * 60;
/**
* save session
*
* @param session
* @throws UnknownSessionException
*/
private void saveSession(Session session) throws UnknownSessionException {
logger.debug("saveSession");
if (session == null || session.getId() == null) {
logger.error("session or session id is null");
return;
}
byte[] key = getByteKey(session.getId());
byte[] value = SerializeUtils.serialize(session);
session.setTimeout(expire * 1000);
RedisUtils.set(key, value, expire);
}
@Override
public void update(Session session) throws UnknownSessionException {
logger.debug("update");
this.saveSession(session);
}
@Override
public void delete(Session session) {
logger.debug("delete");
if (session == null || session.getId() == null) {
logger.error("session or session id is null");
return;
}
RedisUtils.del(this.getByteKey(session.getId()));
}
@Override
public Collection<Session> getActiveSessions() {
logger.debug("getActiveSessions");
Set<Session> sessions = new HashSet<>();
Set<byte[]> keys = RedisUtils.keys(this.keyPrefix + "*");
if (keys != null && keys.size() > 0) {
for (byte[] key : keys) {
Session s = (Session) SerializeUtils.deserialize(RedisUtils.get(key));
sessions.add(s);
}
}
return sessions;
}
@Override
protected Serializable doCreate(Session session) {
logger.debug("doCreate");
Serializable sessionId = this.generateSessionId(session);
this.assignSessionId(session, sessionId);
this.saveSession(session);
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
logger.debug("doReadSession,sessionId:{}", sessionId);
if (sessionId == null) {
logger.error("session id is null");
return null;
}
try {
return (Session) SerializeUtils.deserialize(RedisUtils.get(this.getByteKey(sessionId)));
} catch (Exception e) {
logger.error("Failed to deserialize", e);
return null;
}
}
/**
* 获得byte[]型的key
*
* @param sessionId
* @return
*/
private byte[] getByteKey(Serializable sessionId) {
String preKey = this.keyPrefix + sessionId;
return preKey.getBytes(StandardCharsets.UTF_8);
}
/**
* Returns the Redis session keys
* prefix.
*
* @return The prefix
*/
public String getKeyPrefix() {
return keyPrefix;
}
/**
* Sets the Redis sessions key
* prefix.
*
* @param keyPrefix The prefix
*/
public void setKeyPrefix(String keyPrefix) {
this.keyPrefix = keyPrefix;
}
public int getExpire() {
return expire;
}
public void setExpire(int expire) {
this.expire = expire;
}
}
说的比较笼统,只要自己debug过的就会很清楚,弄懂之后shiro这块登陆和身份权限验证也就简单多了。自己也可以写一套了,不需要这么的复杂,
有其他的业务也可以直接扩展。