基本功能
shiro 完成工作的流程
架构
主要组件
SecurityManager
:安全管理器,Shiro
最核心的组件。Authenticator
:认证器,认证AuthenticationToken
是否有效。AuthenticationToken
:认证信息,比如用户名和密码。SessionManager
:会话管理器,会话Session
就是用户使用程序的一定时间内携带的数据。Subject
:当前操作主体,即用户。SubjectContext
:用户的上下文数据对象。ThreadContext
:线程上下文对象,负责绑定Subject
对象到当前线程。
SecurityManager
继承了授权器、认证器、会话管理器三个接口,用于提供各种安全管理的服务。
SecurityManager
提供三个方法:登录、退出登录、创建 Subject
public interface SecurityManager extends Authenticator, Authorizer, SessionManager {
Subject login(Subject subject, AuthenticationToken authenticationToken) throws AuthenticationException;
void logout(Subject subject);
Subject createSubject(SubjectContext context);
}
如何获取 Subject 对象
Shiro
提供了一个工具类:SecurityUtils
,用于获取 SecurityManager
和 Subject
对象。
public static Subject getSubject() {
// 1、从线程上下文对象中获取
Subject subject = ThreadContext.getSubject();
// 2、如果没有,就构造一个,再绑定到线程上下文对象中
if (subject == null) {
// 使用 Subject的内部类Builder构造
subject = (new Subject.Builder()).buildSubject();
ThreadContext.bind(subject);
}
return subject;
}
其中,从线程上下文对象中获取:
public abstract class ThreadContext {
// ThreadContext_SUBJECT_KEY
public static final String SUBJECT_KEY = ThreadContext.class.getName() + "_SUBJECT_KEY";
// 使用一个 ThreadLocal 对象来保存 Subject对象
private static final ThreadLocal<Map<Object, Object>> resources = new InheritableThreadLocalMap<Map<Object, Object>>();
public static Subject getSubject() {
// 从resources中获取
return (Subject) get(SUBJECT_KEY);
}
}
如果没有,构造一个 Subject
对象:
public Subject buildSubject() {
// 最终使用安全管理器创建
return this.securityManager.createSubject(this.subjectContext);
}
绑定:实际上就放入线程上下文对象中的 ThreadLocal
成员
public static void bind(Subject subject) {
if (subject != null) {
put(SUBJECT_KEY, subject);
}
}
大致流程:SubjectContext
怎么来的 ?
会话管理器
会话接口 Session
:用于保存用户的会话数据。
public interface Session {
Serializable getId();
Date getStartTimestamp();
Date getLastAccessTime();
long getTimeout() throws InvalidSessionException;
void setTimeout(long maxIdleTimeInMillis) throws InvalidSessionException;
String getHost();
void touch() throws InvalidSessionException;
void stop() throws InvalidSessionException;
Collection<Object> getAttributeKeys() throws InvalidSessionException;
Object getAttribute(Object key) throws InvalidSessionException;
void setAttribute(Object key, Object value) throws InvalidSessionException;
Object removeAttribute(Object key) throws InvalidSessionException;
}
常用的实现:
会话管理器的方法:开启会话和创建会话
public interface SessionManager {
// SessionContext:创建会话时的上下文对象,用于保存会话的属性,实际上就是一个map
Session start(SessionContext context);
Session getSession(SessionKey key) throws SessionException;
}
会话管理器主要用于创建、删除、操作会话。常见实现:
一般使用 DefaultWebSessionManager
。
会话管理器的基础实现:AbstractNativeSessionManager
public abstract class AbstractNativeSessionManager extends AbstractSessionManager implements NativeSessionManager, EventBusAware {
private EventBus eventBus;
// 使用Session监听器对Session的状态进行监听
private Collection<SessionListener> listeners;
// 根据SessionContext开启一个新的Session
public Session start(SessionContext context) {
// createSession()是一个抽象方法,由子类实现
Session session = createSession(context);
applyGlobalSessionTimeout(session);
// 钩子方法,空实现,留给子类重写
onStart(session, context);
// 遍历每一个监听器,对session调用onStart()方法
notifyStart(session);
return createExposedSession(session, context);
}
protected void applyGlobalSessionTimeout(Session session) {
session.setTimeout(getGlobalSessionTimeout());
// 钩子方法
onChange(session);
}
protected Session createExposedSession(Session session, SessionContext context) {
return new DelegatingSession(this, new DefaultSessionKey(session.getId()));
}
...
}
1、定义了开启新会话的方法 start()
的骨架
2、创建 Session
对象的方法由子类实现,子类可以创建不同的实例
3、这里保留了两个钩子方法:onStart()
、onChange()
,可以由子类扩展
4、start()
方法不会直接将创建的 Session
对象返回,而是交给 DelegatingSession
管理,构造器:将当前会话管理器对象、Session
的 sessionId
作为参数
protected Session createExposedSession(Session session, SessionKey key) {
// SessionKey实际就是 sessionId 的对象形式
return new DelegatingSession(this, new DefaultSessionKey(session.getId()));
}
AbstractNativeSessionManager
获取 Session
的方法:
public Session getSession(SessionKey key) throws SessionException {
Session session = lookupSession(key);
return session != null ? createExposedSession(session, key) : null;
}
private Session lookupSession(SessionKey key) throws SessionException {
if (key == null) {
throw new NullPointerException("SessionKey argument cannot be null.");
}
// 抽象方法
return doGetSession(key);
}
确保不会直接将 Session
对象暴露出去,而是暴露一个 DelegatingSession
对象。
监听器相关的三个方法: Session
启动、停止、过期的时候,会回调这几个方法
protected void notifyStart(Session session) {
for (SessionListener listener : this.listeners) {
listener.onStart(session);
}
}
protected void notifyStop(Session session) {
Session forNotification = beforeInvalidNotification(session);
for (SessionListener listener : this.listeners) {
listener.onStop(forNotification);
}
}
protected void notifyExpiration(Session session) {
Session forNotification = beforeInvalidNotification(session);
for (SessionListener listener : this.listeners) {
listener.onExpiration(forNotification);
}
}
Session 实例是怎么创建出来的 ?
SessionContext
的注释:
USAGE: Most Shiro end-users will never use a SubjectContext instance directly and instead will call the Subject.getSession() or Subject.getSession(boolean) methods (which will usually use SessionContext instances to start a session with the application’s SessionManager.
大部分的 shiro
使用者不会直接使用 SubjectContext
来创建 Session
对象,而是使用Subject
的getSession()
方法来创建。DelegatingSubject
的实现:
public Session getSession() {
return getSession(true);
}
/**
* 返回与当前Subject关联的Session对象,如果没有,根据 create 来决定是否创建一个新的会话对象
*/
public Session getSession(boolean create) {
...
if (this.session == null && create) {
...
// 1、创建一个DefaultSessionContext对象
// 如果this.host不为空,就设置进去
SessionContext sessionContext = createSessionContext();
// 2、见AbstractNativeSessionManager的实现
Session session = this.securityManager.start(sessionContext);
// 3、封装成StoppingAwareProxiedSession对象
this.session = decorate(session);
}
return this.session;
}
1、SessionContext
会直接创建一个 DefaultSessionContext
的实现,实际上就是一个 map ,只是提供了一些方法来方便获取属性。
2、调用安全管理器的 start()
方法来创建 Session
对象,AbstractNativeSessionManager
中定义了 start()
方法的基本框架:
public Session start(SessionContext context) {
// 真正创建Session实例的方法
// 抽象方法,有子类实现
Session session = createSession(context);
// 设置Session的超时时间
// 每次Session发生属性的修改时,都会调用 onChange() 钩子方法,由子类扩展
applyGlobalSessionTimeout(session);
onStart(session, context);
notifyStart(session);
// 会将当前的Session封装成一个DelegatingSession对象返回,不会暴露当前的Session
return createExposedSession(session, context);
}
子类对 createSession()
的实现:AbstractValidatingSessionManager
类
protected Session createSession(SessionContext context) throws AuthorizationException {
// 规定在创建之前做一次校验
enableSessionValidationIfNecessary();
// 抽象方法
return doCreateSession(context);
}
DefaultSessionManager
类对 doCreateSession()
的实现:
protected Session doCreateSession(SessionContext context) {
// getSessionFactory().createSession(context)
// 由会话工厂来创建Session对象
Session s = newSessionInstance(context);
...
// 使用SessionDAO来将会话对象Session保存起来
create(s);
return s;
}
对于默认的会话管理器:DefaultSessionManager
,构造器:
public DefaultSessionManager() {
this.deleteInvalidSessions = true;
this.sessionFactory = new SimpleSessionFactory();
this.sessionDAO = new MemorySessionDAO();
}
默认使用 SimpleSessionFactory
作为Session工厂,创建的Session实现为 SimpleSession
对象,使用 MemorySessionDAO
作为 SessionDAO 的实现,将每一个用户的 Session
保存在内存中:
public class MemorySessionDAO extends AbstractSessionDAO {
private ConcurrentMap<Serializable, Session> sessions;
public MemorySessionDAO() {
this.sessions = new ConcurrentHashMap<Serializable, Session>();
}
....
}
3、将 Session 对象封装成 StoppingAwareProxiedSession 返回
认证器接口
public interface Authenticator {
// 认证方法
public AuthenticationInfo authenticate(AuthenticationToken authenticationToken) throws AuthenticationException;
}
AuthenticationToken
接口:保存了当前用户的待认证信息
public interface AuthenticationToken extends Serializable {
Object getPrincipal();
Object getCredentials();
}
认证通过后,生成一个 AuthenticationInfo
对象:
public interface AuthenticationInfo extends Serializable {
PrincipalCollection getPrincipals();
Object getCredentials();
}
Realm 接口
可以理解成一个 DAO,主要用于身份验证和授权,一般在项目中自定义一个,然后配置到安全管理器中去,安全管理器就会使用它访问数据库,如:
@Bean
public DefaultWebSecurityManager securityManager(ShiroRealm shiroRealm,
@Nullable RedisCacheManager redisCacheManager,
@Nullable EhCacheManager ehCacheManager,
DefaultWebSessionManager sessionManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 配置 SecurityManager,并注入 shiroRealm
securityManager.setRealm(shiroRealm);
// 其他配置...
return securityManager;
}
Realm
接口:
public interface Realm {
String getName();
boolean supports(AuthenticationToken token);
// 重点方法
// 根据 token 的信息去访问数据库,构造出 AuthenticationInfo 对象并返回
AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;
}
继承关系:一般会自定义一个 AuthorizingRealm
的子类,注入 Spring IOC 容器,Shiro
提供的其他实现不够实用。
AuthorizingRealm
源码:
public abstract class AuthorizingRealm extends AuthenticatingRealm....{
// 额外定义了授权方法
protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals);
....
}
Subject 接口
表示当前正在访问程序的用户。接口:
public interface Subject {
// 1、认证相关 ======================================================
// 返回用户的唯一标识,比如用户名
Object getPrincipal();
PrincipalCollection getPrincipals();
// 是否已经认证通过
boolean isAuthenticated();
// 认证登录
void login(AuthenticationToken token) throws AuthenticationException;
// 认证登出
void logout();
// 2、授权相关 =====================================================
// 是否有 permission 权限
boolean isPermitted(String permission);
// 检测是否有权限,如果没有将抛出异常
void checkPermission(String permission) throws AuthorizationException;
// ....
// 3、Session 相关 ================================================
Session getSession(); // 获取,不存在就创建
// 内部类:构造器
public static class Builder {
private final SubjectContext subjectContext;
private final SecurityManager securityManager;
...
}
...
}
认证流程
使用 Shiro
进行认证:
// 1、根据输入的参数:用户名、密码,来构造一个 UsernamePasswordToken
UsernamePasswordToken token = new UsernamePasswordToken
(username, password, rememberMe); //host=null
// 2、登录
SecurityUtils.getSubject().login(token);
UsernamePasswordToken
的继承关系:
构造器:
public UsernamePasswordToken(final String username, final char[] password,
final boolean rememberMe, final String host) {
this.username = username;
this.password = password;
this.rememberMe = rememberMe;
this.host = host;
}
最终使用 Subject
的 login()
方法完成登录:void login(AuthenticationToken token) throws AuthenticationException
,如果不成功,就会抛出一个 AuthenticationException
认证异常类的实例,shiro
提供了一些子类实现来标识认证失败的原因。
DelegatingSubject
的 login()
方法:
public void login(AuthenticationToken token) throws AuthenticationException {
...
// 由安全管理器执行登录操作
// 如果登录成功,返回更新后的 Subject 对象,否则抛出异常
// protected transient SecurityManager securityManager
Subject subject = securityManager.login(this, token);
// 做一些成员变量的赋值...
}
一般在配置类中配置 DefaultWebSecurityManager
的bean,继承关系:
直接继承了父类的 login()
方法:
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
// private Authenticator authenticator
// 调用认证器的认证方法,如果成功,就返回AuthenticationInfo对象,否则抛出异常
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
}
// 登录成功,创建一个新的 Subject 对象返回
Subject loggedIn = createSubject(token, info, subject);
onSuccessfulLogin(token, info, loggedIn);
return loggedIn;
}
大致流程:
认证器如何完成认证操作 ?
安全管理器 DefaultWebSecurityManager
的父类 AuthenticatingSecurityManager
的构造器中:
private Authenticator authenticator;
public AuthenticatingSecurityManager() {
super();
// 所以,DefaultWebSecurityManager中默认使用的实现为ModularRealmAuthenticator
this.authenticator = new ModularRealmAuthenticator();
}
AbstractAuthenticator
的认证方法:
// final,子类不能再重写
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
...
AuthenticationInfo info;
info = doAuthenticate(token);
...
return info;
}
// 具体的认证逻辑
protected abstract AuthenticationInfo doAuthenticate(AuthenticationToken token) throws AuthenticationException;
这个类使用 final
标记了 authenticate()
方法,子类不能再重写,在 doAuthenticate()
执行前后做了一些校验、异常处理的操作,然后提供抽象方法 doAuthenticate()
给子类,去实现真正的认证逻辑。
ModularRealmAuthenticator
实现的 doAuthenticate()
方法:
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
assertRealmsConfigured();
// private Collection<Realm> realms
// 最终使用Realm与数据库打交道
Collection<Realm> realms = getRealms();
if (realms.size() == 1) {
// 一般自定义一个AuthorizingRealm的子类
// 放入IOC容器
// 核心方法:
// AuthenticationInfo info = realm.getAuthenticationInfo(token)
return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
} else {
return doMultiRealmAuthentication(realms, authenticationToken);
}
}
AuthenticatingRealm
中实现了 getAuthenticationInfo()
方法:同样是使用 final
标记,然后提供一个抽象方法 :
protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;
可以自定义一个 ShiroRealm
如:
public class ShiroRealm extends AuthorizingRealm {
...
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 获取用户输入的用户名和密码
String username = (String) token.getPrincipal();
String password = new String((char[]) token.getCredentials());
// 通过用户名到数据库查询用户信息
User user = this.userService.findByName(username);
if (user == null || !StringUtils.equals(password, user.getPassword())) {
throw new IncorrectCredentialsException("用户名或密码错误!");
}
if (User.STATUS_LOCK.equals(user.getStatus())) {
throw new LockedAccountException("账号已被锁定,请联系管理员!");
}
String deptIds = this.userDataPermissionService.findByUserId(String.valueOf(user.getUserId()));
user.setDeptIds(deptIds);
return new SimpleAuthenticationInfo(user, password, getName());
}
...
}
大致流程:
自定义的 Realm
实现建议继承 AuthorizingRealm
,集成认证和授权两个模块。
Shiro 的过滤器
通过 AbstractShiroFilter
可以实现对不同请求的过滤,如:
// ShiroConfig.java
// ShiroFilterFactoryBean 用于创建 SpringShiroFilter
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
// <1> 创建 ShiroFilterFactoryBean 对象,用于创建 ShiroFilter 过滤器
ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
// <2> 设置 SecurityManager
filterFactoryBean.setSecurityManager(this.securityManager());
// <3> 设置 URL 们
filterFactoryBean.setLoginUrl("/login"); // 登录 URL
filterFactoryBean.setSuccessUrl("/login_success"); // 登录成功 URL
filterFactoryBean.setUnauthorizedUrl("/unauthorized"); // 无权限 URL
// <4> 设置 URL 的权限配置
filterFactoryBean.setFilterChainDefinitionMap(this.filterChainDefinitionMap());
return filterFactoryBean;
}
private Map<String, String> filterChainDefinitionMap() {
Map<String, String> filterMap = new LinkedHashMap<>(); // 注意要使用有序的 LinkedHashMap ,顺序匹配
filterMap.put("/test/echo", "anon"); // 允许匿名访问
filterMap.put("/test/admin", "roles[ADMIN]"); // 需要 ADMIN 角色
filterMap.put("/test/normal", "roles[NORMAL]"); // 需要 NORMAL 角色
filterMap.put("/logout", "logout"); // 退出
filterMap.put("/**", "authc"); // 默认剩余的 URL ,需要经过认证
return filterMap;
}
<3>:
GET /login
为登录页面,POST /login
为登录请求/login_success
:登录成功后重定向/unauthorized
:没有权限时重定向
Shiro
提供了提供了一些实用的过滤器,在枚举类 DefaultFilter
里面可见:
public enum DefaultFilter {
// 匿名过滤器
anon(AnonymousFilter.class),
// 认证过滤器
authc(FormAuthenticationFilter.class),
authcBasic(BasicHttpAuthenticationFilter.class),
authcBearer(BearerHttpAuthenticationFilter.class),
// 登出过滤器
logout(LogoutFilter.class),
noSessionCreation(NoSessionCreationFilter.class),
// 权限过滤器
perms(PermissionsAuthorizationFilter.class),
port(PortFilter.class),
rest(HttpMethodPermissionFilter.class),
// 角色过滤器
roles(RolesAuthorizationFilter.class),
ssl(SslFilter.class),
user(UserFilter.class),
invalidRequest(InvalidRequestFilter.class);
private final Class<? extends Filter> filterClass;
...
}
匿名过滤器:直接返回true,即可以匿名访问
@Override
protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) {
// Always return true since we allow access to anyone
return true;
}
认证过滤器:FormAuthenticationFilter
- 经过认证的用户可以直接访问。
- 匿名用户,但请求的是
loginUrl
(在创建ShiroFilterFactoryBean
的时候设置,默认为login.jsp
)时,如果是GET
请求,就跳转到登录页,如果为POST
,就会该用户进行认证。 - 匿名用户,请求的不是
loginUrl
,就先登录,登录成功后再重定向到该 URL。
FormAuthenticationFilter
的父类 AccessControlFilter
中:
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
}
1、先判断是否可以访问 isAccessAllowed()
,核心逻辑:
// AuthenticationFilter.java
subject.isAuthenticated() && subject.getPrincipal() != null
或者:
(!isLoginRequest(request, response) && isPermissive(mappedValue))
即判断当前用户是否认证过,或者拥有访问该 URL 的权限。
如果满足条件,直接返回true。
2、如果是匿名用户,执行 onAccessDenied()
:
// FormAuthenticationFilter.java
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
if (isLoginRequest(request, response)) { // 是登录请求
// POST 请求, 执行登录逻辑
if (isLoginSubmission(request, response)) {
...
return executeLogin(request, response);
} else {
....
//allow them to see the login page ;)
// 要去登录页,直接放行
return true;
}
} else { // 不是登录请求
...
// 保存这个请求并且重定向到登录页
// 保存:实际上就是放入当前Subject的Session中,key为shiroSavedRequest
saveRequestAndRedirectToLogin(request, response);
return false;
}
}
流程:
角色过滤器:RolesAuthorizationFilter
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {
Subject subject = getSubject(request, response);
// 哪些角色可以访问这个URL
String[] rolesArray = (String[]) mappedValue;
if (rolesArray == null || rolesArray.length == 0) {
//no roles specified, so nothing to check - allow access.
return true;
}
Set<String> roles = CollectionUtils.asSet(rolesArray);
return subject.hasAllRoles(roles);
}
权限过滤器:PermissionsAuthorizationFilter
,拥有指定权限的用户才可以访问。
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {
Subject subject = getSubject(request, response);
// 访问URL所需的权限
String[] perms = (String[]) mappedValue;
boolean isPermitted = true;
if (perms != null && perms.length > 0) {
if (perms.length == 1) {
if (!subject.isPermitted(perms[0])) {
isPermitted = false;
}
} else {
if (!subject.isPermittedAll(perms)) {
isPermitted = false;
}
}
}
return isPermitted;
}
Shiro 访问控制过滤器:AccessControlFilter
AccessControlFilter
是 Shiro
的核心过滤器,定义了判断控制访问的逻辑:
public abstract class AccessControlFilter extends PathMatchingFilter {
public static final String DEFAULT_LOGIN_URL = "/login.jsp";
public static final String GET_METHOD = "GET";
public static final String POST_METHOD = "POST";
private String loginUrl = DEFAULT_LOGIN_URL;
// 判断是否可以访问
protected abstract boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception;
// 访问被拒绝后的行为,如认证过滤器就是跳到登录页
protected boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return onAccessDenied(request, response);
}
protected abstract boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception;
// 过滤请求
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
}
...
}
权限过滤器、角色过滤器等过滤器都是在重写 isAccessAllowed()
和 onAccessDenied()
方法,从而实现认证和授权。
AccessControlFilter
的创建实现:主要分为认证和授权两个模块
Shiro 注解
Shiro
提供了几个注解,可以直接标记在 Spring Web MVC
的 controller
的方法上,如:
@PostMapping("/dept")
// 表示 POST /dept 请求需要对 dept模块的add权限
// 相当于给这个URL配置了一个 PermissionsAuthorizationFilter
@RequiresPermissions("dept:add")
public Result addDept(@Valid Dept dept) {
deptService.createDept(dept);
return new Result().success();
}
作用相当于为该URL添加过滤器。
@RequiresGuest
相当于添加AnonymousFilter
;@RequiresAuthentication
相当于添加FormAuthenticationFilter
;@RequiresUser
相当于添加UserFilter
;@RequiresRoles
相当于添加RolesAuthorizationFilter
;@RequiresPermissions
相当于添加PermissionsAuthorizationFilter
;
Shiro 认证异常
账号异常:
密码异常:
还有一个:
public class UnsupportedTokenException extends AuthenticationException {
...
}
在认证的过程中,没有一个配置好的 Realm
支持当前 AuthenticationToken
实例,就会抛出这个异常。
Shiro 授权异常
扫描 shiro 注解的组件是什么 ?
…