去GitHub搜索shiro,克隆源码,然后会看到里面有samples模块,先从quickstart看起
一、例子源码
首先是配置文件shiro.ini
[users]
root = secret, admin
guest = guest, guest
presidentskroob = 12345, president
darkhelmet = ludicrousspeed, darklord, schwartz
lonestarr = vespa, goodguy, schwartz
[roles]
admin = *
schwartz = lightsaber:*
goodguy = winnebago:drive:eagle5
ini配置中主要配置有四大类:main,users,roles,urls,这里只包含了users,roles。
[main]
#提供了对根对象 securityManager 及其依赖的配置
securityManager=org.apache.shiro.mgt.DefaultSecurityManager
securityManager.realms=$jdbcRealm
…
[users]
#提供了对用户/密码及其角色的配置,用户名=密码,角色 1,角色 2
username=password,role1,role2
…
[roles]
#提供了角色及权限之间关系的配置,角色=权限 1,权限 2
role1=permission1,permission2
…
[urls]
#用于 web,提供了对 web url 拦截相关的配置,url=拦截器[参数],拦截器
/index.html = anon
/admin/** = authc, roles[admin], perms[“permission1”]
java类主方法
public static void main(String[] args) {
//1、工厂解析配置文件
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//2、获取安全管理器
SecurityManager securityManager = factory.getInstance();
//设置安全管理器
SecurityUtils.setSecurityManager(securityManager);
//3、获取当前用户
Subject currentUser = SecurityUtils.getSubject();
//4、获取session
Session session = currentUser.getSession();
session.setAttribute("someKey", "aValue");
String value = (String) session.getAttribute("someKey");
...
if (!currentUser.isAuthenticated()) {
//把登录的信息封装到token中
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
token.setRememberMe(true);
try {
//5、登录
currentUser.login(token);
}catch...
}
...
6、登出
currentUser.logout();
}
二、跟踪源码分析
1、解析配置文件new IniSecurityManagerFactory(“classpath:shiro.ini”)
public IniSecurityManagerFactory(String iniResourcePath) {
this(Ini.fromResourcePath(iniResourcePath));
}
public IniSecurityManagerFactory(Ini config) {
this();
setIni(config);
}
解析配置文件是用了Ini类的fromResourcePath方法。
public static Ini fromResourcePath(String resourcePath) throws ConfigurationException {
...
Ini ini = new Ini();
ini.loadFromPath(resourcePath);
return ini;
}
public void loadFromPath(String resourcePath) throws ConfigurationException {
InputStream is;
try {
is = ResourceUtils.getInputStreamForPath(resourcePath);
} catch...
load(is);
}
public void load(InputStream is) throws ConfigurationException {
...
InputStreamReader isr;
try {
isr = new InputStreamReader(is, DEFAULT_CHARSET_NAME);
} catch...
load(isr);
}
public void load(Reader reader) {
Scanner scanner = new Scanner(reader);
try {
load(scanner);
} finally {
...
}
}
public void load(Scanner scanner) {
//当前解析的配置类别
String sectionName = DEFAULT_SECTION_NAME;
StringBuilder sectionContent = new StringBuilder();
while (scanner.hasNextLine()) {
String rawLine = scanner.nextLine();
String line = StringUtils.clean(rawLine);
//#号和;号开头的跳过
if (line == null || line.startsWith(COMMENT_POUND) || line.startsWith(COMMENT_SEMICOLON)) {
continue;
}
String newSectionName = getSectionName(line);
if (newSectionName != null) {
//解析到达了新的配置类别,所以要把缓存中的内容先解析
addSection(sectionName, sectionContent);
//相当于清空
sectionContent = new StringBuilder();
sectionName = newSectionName;//更新当前要解析的配置类别
...
} else {
//添加内容当缓存
sectionContent.append(rawLine).append("\n");
}
}
//解析缓存的配置
addSection(sectionName, sectionContent);
}
解析配置文件是调用了Ini对象的loadFromPath方法,Ini类主要结构如下
public class Ini implements Map<String, Ini.Section> {
//装每一类配置的内容,如main、users、roles…
private final Map<String, Section> sections;
}
接下来是解析每一类配置
private void addSection(String name, StringBuilder content) {
if (content.length() > 0) {
String contentString = content.toString();
String cleaned = StringUtils.clean(contentString);
if (cleaned != null) {
Section section = new Section(name, contentString);
if (!section.isEmpty()) {
sections.put(name, section);
}
}
}
}
解析的内容存在Section对象里,然后放进sections中缓存。Section是Ini的内部类,主要结构如下
public static class Section implements Map<String, String> {
private final String name;//配置类别:如users、roles
private final Map<String, String> props;//如:root --> 123,admin
}
2、获取安全管理器
public T getInstance() {
T instance;
if (isSingleton()) {//默认是true
if (this.singletonInstance == null) {
this.singletonInstance = createInstance();
}
instance = this.singletonInstance;
} else {
instance = createInstance();
}
if (instance == null) {
throw ...
}
return instance;
}
public T createInstance() {
//如果没提供配置文件,shiro会尝试加载classpath:shiro.ini文件
Ini ini = resolveIni();
T instance;
if (CollectionUtils.isEmpty(ini)) {
...
instance = createDefaultInstance();
if (instance == null) {
throw ...
}
} else {
instance = createInstance(ini);
if (instance == null) {
throw ...
}
}
return instance;
}
最后调用的是IniSecurityManagerFactory的createInstance方法来创建实例
protected SecurityManager createInstance(Ini ini) {
...
SecurityManager securityManager = createSecurityManager(ini);
if (securityManager == null) {
throw ...
}
return securityManager;
}
private SecurityManager createSecurityManager(Ini ini) {
return createSecurityManager(ini, getConfigSection(ini));
}
private Ini.Section getConfigSection(Ini ini) {
//获取[main]的配置
Ini.Section mainSection = ini.getSection(MAIN_SECTION_NAME);
if (CollectionUtils.isEmpty(mainSection)) {
//尝试获取“”的配置
mainSection = ini.getSection(Ini.DEFAULT_SECTION_NAME);
}
return mainSection;
}
在上面解析配置文件的时候可以看到,一开始解析的配置是DEFAULT_SECTION_NAME("“空字符串),所以这里当[main]不存在的时候会尝试获取”"的配置。
接下来开始创建安全管理器
private SecurityManager createSecurityManager(Ini ini, Ini.Section mainSection) {
//添加对象缓存到ReflectionBuilder对象
getReflectionBuilder().setObjects(createDefaults(ini, mainSection));
//处理[main]配置
Map<String, ?> objects = buildInstances(mainSection);
//获取以SECURITY_MANAGER_NAME为key的对象
SecurityManager securityManager = getSecurityManagerBean();
//这里根据securityManager里是否已有Realm对象,没有就返回true
boolean autoApplyRealms = isAutoApplyRealms(securityManager);
if (autoApplyRealms) {
//从缓存的bean中找出realm
Collection<Realm> realms = getRealms(objects);
//设进securityManager里
if (!CollectionUtils.isEmpty(realms)) {
applyRealmsToSecurityManager(realms, securityManager);
}
}
return securityManager;
}
这里涉及到ReflectionBuilder类,个人把它理解成是相当于spring中的容器的概念,它主要负责对bean的实例化和管理,里面还实现了事件订阅监听机制,如bean被初始化后会发布InitializedBeanEvent事件
创建默认的bean如下:
protected Map<String, ?> createDefaults(Ini ini, Ini.Section mainSection) {
Map<String, Object> defaults = new LinkedHashMap<String, Object>();
//创建了一个DefaultSecurityManager
SecurityManager securityManager = createDefaultInstance();
//放入了map中,键为SECURITY_MANAGER_NAME
defaults.put(SECURITY_MANAGER_NAME, securityManager);
//如果[users]、[roles]配置不为空,就会创建一个Realm
if (shouldImplicitlyCreateRealm(ini)) {
Realm realm = createRealm(ini);
if (realm != null) {//放入map中
defaults.put(INI_REALM_NAME, realm);
}
}
...
return defaults;
}
可以看到已经默认创建了一个DefaultSecurityManager,所以就算不配置[main]也不会报错
3、获取当前用户 SecurityUtils.getSubject()
public static Subject getSubject() {
Subject subject = ThreadContext.getSubject();
if (subject == null) {//如果为空就创建
subject = (new Subject.Builder()).buildSubject();
ThreadContext.bind(subject);//绑定到线程
}
return subject;
}
>涉及到了ThreadContext类,结构大致如下
>
public abstract class ThreadContext {
//用于绑定线程数据
private static final ThreadLocal<Map<Object, Object>> resources = new InheritableThreadLocalMap<Map<Object, Object>>();
>
public static Subject getSubject() {
return (Subject) get(SUBJECT_KEY);
}
>
public static Object get(Object key) {
...
Object value = getValue(key);
...
return value;
}
>
private static Object getValue(Object key) {
Map<Object, Object> perThreadResources = resources.get();
return perThreadResources != null ? perThreadResources.get(key) : null;
}
>
public static void bind(Subject subject) {
if (subject != null) {
put(SUBJECT_KEY, subject);
}
}
public static void put(Object key, Object value) {
if (key == null) {
throw ...
}
if (value == null) {
remove(key);
return;
}
ensureResourcesInitialized();
resources.get().put(key, value);
...
}
}
可以看到主要是用到了ThreadLocal类来绑定线程数据
创建用户(new Subject.Builder()).buildSubject():
public Builder() {
this(SecurityUtils.getSecurityManager());
}
public Builder(SecurityManager securityManager) {
if (securityManager == null) {
throw ...
}
this.securityManager = securityManager;
//这里创建一个DefaultSubjectContext对象
this.subjectContext = newSubjectContextInstance();
if (this.subjectContext == null) {
throw ...
}
this.subjectContext.setSecurityManager(securityManager);
}
这里涉及到SubjectContext接口,SubjectContext装载着用户相关的信息,如host、session、token等,DefaultSubjectContext是它的一个实现类。
public Subject buildSubject() {
return this.securityManager.createSubject(this.subjectContext);
}
public Subject createSubject(SubjectContext subjectContext) {
//新建一个subjectContext,并且复制原来的属性
SubjectContext context = copy(subjectContext);
//保证SubjectContext包含安全管理器
context = ensureSecurityManager(context);
//这里如果context的sessionId不为空,就会从SessionManager查询对应sessionId的Session对象,最后将其设进context里
context = resolveSession(context);
context = resolvePrincipals(context);
//开始创建Subject
Subject subject = doCreateSubject(context);
//调用SubjectDAO的save方法
save(subject);
return subject;
}
protected Subject doCreateSubject(SubjectContext context) {
return getSubjectFactory().createSubject(context);
}
public Subject createSubject(SubjectContext context) {
SecurityManager securityManager = context.resolveSecurityManager();
Session session = context.resolveSession();
boolean sessionCreationEnabled = context.isSessionCreationEnabled();
PrincipalCollection principals = context.resolvePrincipals();
boolean authenticated = context.resolveAuthenticated();
String host = context.resolveHost();
return new DelegatingSubject(principals, authenticated, host, session, sessionCreationEnabled, securityManager);
}
最后返回的是DelegatingSubject对象
4、获取当前用户的session对象 currentUser.getSession()
public Session getSession() {
return getSession(true);
}
public Session getSession(boolean create) {
if (this.session == null && create) {
if (!isSessionCreationEnabled()) {
throw ...
}
SessionContext sessionContext = createSessionContext();
Session session = this.securityManager.start(sessionContext);
this.session = decorate(session);
}
return this.session;
}
上面说到Subject的实际类型是DelegatingSubject类型
创建session上下文SessionContext(SessionContext包含sessionId、host信息)
protected SessionContext createSessionContext() {
SessionContext sessionContext = new DefaultSessionContext();
if (StringUtils.hasText(host)) {
sessionContext.setHost(host);
}
return sessionContext;
}
创建session
public Session start(SessionContext context) throws AuthorizationException {
return this.sessionManager.start(context);
}
例子中是通过SessionManager是DefaultSessionManager,其创建的session是一个SimpleSession对象
装饰session对象
protected Session decorate(Session session) {
...
return new StoppingAwareProxiedSession(session, this);
}
StoppingAwareProxiedSession重写了Session的stop方法,主要是在session由于过期等原因被移除的时候,同时也在DelegatingSubject中移除,以防止内存泄露
5、登录currentUser.login(token)
public void login(AuthenticationToken token) throws AuthenticationException {
//从session中移除RUN_AS_PRINCIPALS_SESSION_KEY属性
clearRunAsIdentitiesInternal();
//调用安全管理器进行登录,会返回一个Subject对象
Subject subject = securityManager.login(this, token);
PrincipalCollection principals;
String host = null;
if (subject instanceof DelegatingSubject) {
principals = delegating.principals;
host = delegating.host;
} else {
principals = subject.getPrincipals();
}
if (principals == null || principals.isEmpty()) {
throw ...
}
//更新当前的成员
this.principals = principals;
this.authenticated = true;
if (token instanceof HostAuthenticationToken) {
host = ((HostAuthenticationToken) token).getHost();
}
if (host != null) {
this.host = host;
}
更新session
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 {
info = authenticate(token);
} catch (AuthenticationException ae) {
try {//回调
onFailedLogin(token, ae, subject);
} catch...
throw ae; //继续往上抛
}
//【标记1】生成Subject
Subject loggedIn = createSubject(token, info, subject);
//回调
onSuccessfulLogin(token, info, loggedIn);
return loggedIn;
}
这里的异常继续往上抛就可以给开发者捕获
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
return this.authenticator.authenticate(token);
}
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
if (token == null) {
throw ...
}
AuthenticationInfo info;
try {
info = doAuthenticate(token);
if (info == null) {//用户不存在
throw ...
}
} catch (Throwable t) {
...
try {
notifyFailure(token, ae);//通知监听者
} catch...
throw ae;
}
notifySuccess(token, info);//通知监听者
return info;
}
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
assertRealmsConfigured();
Collection<Realm> realms = getRealms();
if (realms.size() == 1) {
return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
} else {
return doMultiRealmAuthentication(realms, authenticationToken);
}
}
由于例子里只提供了一个realm所以会调用doSingleRealmAuthentication方法
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
if (!realm.supports(token)) {
throw ...
}
AuthenticationInfo info = realm.getAuthenticationInfo(token);
if (info == null) {
throw ...
}
return info;
}
最后是调用了realm的getAuthenticationInfo来完成验证
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//尝试从缓存中获取
AuthenticationInfo info = getCachedAuthenticationInfo(token);
if (info == null) {
//缓存获取失败,则通过其他方式获取,通常是查数据库
info = doGetAuthenticationInfo(token);
if (token != null && info != null) {
//根据是否需要缓存来进行缓存
cacheAuthenticationInfoIfPossible(token, info);
}
}
if (info != null) {//开始比对验证
assertCredentialsMatch(token, info);
} else {
//用户不存在
}
return info;
}
上面可以看到通过doGetAuthenticationInfo来获取用户信息,这也是我们平时自己实现realm要实现的方法。
比对验证
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
//使用验证器进行验证
CredentialsMatcher cm = getCredentialsMatcher();
if (cm != null) {
if (!cm.doCredentialsMatch(token, info)) {
//验证失败
throw new IncorrectCredentialsException(msg);
}
} else {
//不存在验证器
throw ...
}
}
利用验证器来进行验证
【标记1】如果都验证通过了,就会创建一个Subject返回
protected Subject createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing) {
//创建了一个DefaultSubjectContext
SubjectContext context = createSubjectContext();
context.setAuthenticated(true);//设置已经认证
context.setAuthenticationToken(token);
context.setAuthenticationInfo(info);
if (existing != null) {
context.setSubject(existing);
}
return createSubject(context);
}
这里的SubjectContext会把认证的标记为设置成true,然后就是创建Subject,这个上面已经分析过,最后会返回一个DelegatingSubject。安全管理器登录逻辑就完成了。
接下里就是对安全管理器登录逻辑返回的Subject信息进行复制到当前subject
PrincipalCollection principals;
String host = null;
if (subject instanceof DelegatingSubject) {
DelegatingSubject delegating = (DelegatingSubject) subject;
host = delegating.host;
} else {
principals = subject.getPrincipals();
}
if (principals == null || principals.isEmpty()) {
throw ...
}
this.principals = principals;//复制
this.authenticated = true;//标志已认证
if (token instanceof HostAuthenticationToken) {
host = ((HostAuthenticationToken) token).getHost();
}
if (host != null) {
this.host = host;
}
Session session = subject.getSession(false);
if (session != null) {
this.session = decorate(session);//装饰session
} else {
this.session = null;
}
6、当前用户登出currentUser.logout()
public void logout() {
try {
//清楚session中的RUN_AS_PRINCIPALS_SESSION_KEY属性
clearRunAsIdentitiesInternal();
this.securityManager.logout(this);
} finally {//清空信息
this.session = null;
this.principals = null;
this.authenticated = false;
}
}
public void logout(Subject subject) {
if (subject == null) {
throw ...
}
beforeLogout(subject);//删除RememberMe管理器中的相关信息
PrincipalCollection principals = subject.getPrincipals();
if (principals != null && !principals.isEmpty()) {
Authenticator authc = getAuthenticator();
if (authc instanceof LogoutAware) {
//这里可以清理缓存和通知监听器
((LogoutAware) authc).onLogout(principals);
}
}
try {
delete(subject);//这里调用subjectDAO的delete方法
} catch (Exception e) {
if (log.isDebugEnabled()) {
String msg = "Unable to cleanly unbind Subject. Ignoring (logging out).";
log.debug(msg, e);
}
} finally {
try {
stopSession(subject);//设置session过期
} catch (Exception e) {
if (log.isDebugEnabled()) {
String msg = "Unable to cleanly stop Session for Subject [" + subject.getPrincipal() + "] " +
"Ignoring (logging out).";
log.debug(msg, e);
}
}
}
}
总结:
shiro框架可以说是化繁为简,各种组件各司其职,分工明确,值得深入学习。