一、关于shiro
一个shiro的配置案例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
| <?xml version="1.0" encoding="UTF-8"?> <beans>
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="${zheng.upms.sso.server.url}"/> <property name="successUrl" value="${zheng.upms.successUrl}"/> <property name="unauthorizedUrl" value="${zheng.upms.unauthorizedUrl}"/> <property name="filters"> <util:map> <entry key="authc" value-ref="upmsAuthenticationFilter"/> </util:map> </property> <property name="filterChainDefinitions"> <value> /manage/** = upmsSessionForceLogout,authc /manage/index = user /druid/** = user /swagger-ui.html = user /resources/** = anon /** = anon </value> </property> </bean>
<bean id="upmsAuthenticationFilter" class="com.zheng.upms.client.shiro.filter.UpmsAuthenticationFilter"/>
<bean id="upmsSessionForceLogout" class="com.zheng.upms.client.shiro.filter.UpmsSessionForceLogoutFilter"/>
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realms"> <list><ref bean="upmsRealm"/></list> </property> <property name="sessionManager" ref="sessionManager"/> <property name="rememberMeManager" ref="rememberMeManager"/> </bean>
<bean id="upmsRealm" class="com.zheng.upms.client.shiro.realm.UpmsRealm"></bean>
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <property name="globalSessionTimeout" value="${zheng.upms.session.timeout}"/> <property name="sessionDAO" ref="sessionDAO"/> <property name="sessionIdCookieEnabled" value="true"/> <property name="sessionIdCookie" ref="sessionIdCookie"/> <property name="sessionValidationSchedulerEnabled" value="false"/> <property name="sessionListeners"> <list><ref bean="sessionListener"/></list> </property> <property name="sessionFactory" ref="sessionFactory"/> </bean>
<bean id="sessionDAO" class="com.zheng.upms.client.shiro.session.UpmsSessionDao"/>
<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> <property name="httpOnly" value="true"/> <property name="maxAge" value="-1"/> <property name="name" value="${zheng.upms.session.id}"/> </bean>
<bean id="sessionListener" class="com.zheng.upms.client.shiro.listener.UpmsSessionListener"/>
<bean id="sessionFactory" class="com.zheng.upms.client.shiro.session.UpmsSessionFactory"/>
<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager"> <property name="cipherKey" value="#{T(org.apache.shiro.codec.Base64).decode('4AvVhmFLUs0KTA3Kprsdag==')}"/> <property name="cookie" ref="rememberMeCookie"/> </bean>
<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> <constructor-arg value="rememberMe"/> <property name="httpOnly" value="true"/> <property name="maxAge" value="${zheng.upms.rememberMe.timeout}"/> </bean>
<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean"> <property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager"/> <property name="arguments" ref="securityManager"/> </bean>
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"/> <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager"/> </bean>
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
</beans>
|
可见, 三大模块:securityManager、Realm、subject
其中SecurityManager是链接的主体,讲Realm注入其中,并利用Subject subject = SecurityUtils.getSubject();
可以获得当前的subject
二、subject.login()发生了什么
参考
所以最终还是回归到了我们自定义的doGetAuthenticationInfo(AuthenticationToken authenticationToken)
方法。
三、权限验证
(1) 可以简单书写如下:
1 2 3 4 5 6 7 8 9
| protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException { UsernamePasswordToken token = (UsernamePasswordToken) authcToken; User user = accountManager.findUserByUserName(token.getUsername()); if (user != null) { return new SimpleAuthenticationInfo(user.getUserName(), user.getPassword(), getName()); } else { return null; } }
|
但是这时候我们就回有一疑问:这里面都没有验证密码?这个其实是交给credentialsMatcher
去验证的。所以我们可以自定义一个bean,其实现类继承自SimpleCredentialsMatcher
,复写doCredentialsMatch
方法:
1 2 3 4 5 6 7 8 9
| public class MyCredentialsMatcher extends SimpleCredentialsMatcher { @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token; String password = String.valueOf(usernamePasswordToken.getPassword()); String dbpassword = (String) info.getCredentials(); return this.equals(password, dbpassword); } }
|
参考一–参考二
最后MyCredentialsMatcher这个bean注入自定义Realm中。
(2) 也可以不用自定义MyCredentialsMatcher
,直接在doGetAuthenticationInfo
把密码校验给校验了。zheng项目就是这么做的。
四、realm和subject的个人理解
realm可以理解为安全数据源。
自定义的realm,如继承自AuthorizingRealm
,实现了doGetAuthenticationInfo(AuthenticationToken authenticationToken)
(这个方法之后用户就获得了用户是否验证通过信息保存到subject对象中)和doGetAuthorizationInfo(PrincipalCollection principalCollection)
(这个方法之后,就可以检查用户是否具备了某些权限保存到了subject对象中)。在业务代码中我们可以这样判断authenticate和authorization结果:
1 2 3
| Subject subject = SecurityUtils.getSubject(); subject.hasRole("admin"); subject.isAuthenticated();
|
上面理解是错误的,实际上是:
Subject自己不会实现相应的身份验证/授权逻辑,而是通过DelegatingSubject委托给SecurityManager实现;及可以理解为Subject是一个面门。参考
subject的使用:
- 身份验证(login)
- 授权(hasRole*/isPermitted或checkRole/checkPermission*)
- 将相应的数据存储到会话(Session)
- 切换身份(RunAs)/多线程身份传播
- 退出
1、首先调用 Subject.isPermitted*/hasRole*接口,其会委托给 SecurityManager,而
SecurityManager 接着会委托给 Authorizer;
2、Authorizer 是真正的授权者,如果我们调用如 isPermitted(“user:view”),其首先会通过PermissionResolver
把字符串转换成相应的 Permission 实例;
3、在进行授权之前,其会调用相应的 Realm 获取 Subject 相应的角色/权限用于匹配传入的
角色/权限;
4、Authorizer 会判断 Realm 的角色/权限是否和传入的匹配,如果有多个 Realm,会委托给
ModularRealmAuthorizer 进行循环判断,如果匹配如 isPermitted*/hasRole*会返回 true,否则返回 false 表示授权失败
关于授权:
public abstract class AuthorizingRealm extends AuthenticatingRealm implements Authorizer, Initializable, PermissionResolverAware, RolePermissionResolverAware
可见AuthorizingRealm实现了Authorizer,所有就有了isPermitted/hasRole方法
五、SecurityManager和SecurityUtils设置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| public void test() {
DefaultSecurityManager securityManager = new DefaultSecurityManager();
ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator(); authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy()); securityManager.setAuthenticator(authenticator);
ModularRealmAuthorizer authorizer = new ModularRealmAuthorizer(); authorizer.setPermissionResolver(new WildcardPermissionResolver()); securityManager.setAuthorizer(authorizer);
DruidDataSource ds = new DruidDataSource(); ds.setDriverClassName("com.mysql.jdbc.Driver"); ds.setUrl("jdbc:mysql://localhost:3306/shiro"); ds.setUsername("root"); ds.setPassword("");
JdbcRealm jdbcRealm = new JdbcRealm(); jdbcRealm.setDataSource(ds); jdbcRealm.setPermissionsLookupEnabled(true); securityManager.setRealms(Arrays.asList((Realm) jdbcRealm));
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("zhang", "123"); subject.login(token);
Assert.assertTrue(subject.isAuthenticated()); }
|