码迷,mamicode.com
首页 > 编程语言 > 详细

Java安全框架shiro

时间:2017-03-31 21:48:53      阅读:500      评论:0      收藏:0      [点我收藏+]

标签:proc   mesh   类的方法   名称   ons   ace   rda   attribute   man   

shiro是一个强大而且简单易用的Java安全框架,主要功能有认证(就是登陆验证),授权(就是权限管理),加密(就是密码加密),session管理。适用于各种大型或者小型企业应用。和Spring Security比较而言,确实更加简单而且灵活易懂。
 
1. shiro中的重要概念
要理解shiro,先要理解框架的几个概念:
1) Subject: 代表当前登陆或者访问的用户;
2)Principals:一般指用户名等,唯一表明Subject身份也就是当前用户身份的东西;
3)Credentials:凭证,一般指密码,对当前登陆用户进行验证;
4)Realms:域,一般是指存储用户信息(用户名,密码,权限,角色)的数据库,也就是保存用户权限等信息的数据源;
5)SecurityManager:shiro安全管理的顶级对象。它集合或者说调用所有其它相关组件,负责所有安全和权限相关处理过程,就像一个中央集权政府;
 
3. Authentication认证子系统
认证子系统,就是处理用户登录,验证用户登录。我们前面讲到Subject代表当前用户,而Principal和credential分别就代表用户名和密码。登录认证时,其实就是调用的 Subject.login(AuthenticationToken token)方法,AuthenticationToken是一个接口:
public interface AuthenticationToken extends Serializable { Object getPrincipal(); Object getCredentials(); }
 
一般我们new一个UsernamePasswordToken的对象:UsernamePasswordToken token = new UsernamePasswordToken("xxxusername", "xxxpassword");, 然后 subject.login(token); 就前去登录
@RequestMapping(value="/loginController", method=RequestMethod.POST) public String login(String userName, String password, String rememberMe, String type, HttpServletRequest req) { String error = null; Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(userName, password); if(rememberMe != null && "true".equals(rememberMe)) token.setRememberMe(true); // 记住我 try { subject.login(token); } catch (UnknownAccountException | IncorrectCredentialsException e1) { error = "用户名或密码错误"; }catch(ExcessiveAttemptsException e){ userService.lockAccountByNo(no); // 锁定账户 error = "超过了尝试登录的次数,您的账户已经被锁定。"; }catch (AuthenticationException e) { // 其他错误 if(e.getMessage() != null) error = "发生错误:" + e.getMessage(); else error = "发生错误,无法登录。"; }
 
其实我们也可以使用 AuthenticatingRealm 的子类 AuthorizingRealm,它本来是用于权限认证的Realm,但是因为他继承了 AuthenticatingRealm,所以实际上我们只要继承 AuthorizingRealm,然后实现它的抽象方法就行了。同时搞定 登录认证 和 权限认证
public class UserRealm extends AuthorizingRealm { @Autowired private UserService userService; @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String userName = (String)principals.getPrimaryPrincipal(); User user = userService.getUserByName(userName); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); authorizationInfo.setRoles(userService.findRolesByUserId(user.getId())); authorizationInfo.setStringPermissions(userService.findPermissionsByUserId(user.getId())); return authorizationInfo; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String userName= (String)token.getPrincipal(); User user = userService.getUserByName(userName); if(user == null) { throw new UnknownAccountException();//没找到账户 } if(user.getLocked() == 0) { throw new LockedAccountException(); //帐号锁定 } if(user.getLocked() == 2){ throw new AuthenticationException("account was inactive"); } SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( user.getUserName(), // 用户名 user.getPassword(), // 密码 ByteSource.Util.bytes(user.getCredentialsSalt()), // salt getName() // realm name ); return authenticationInfo; } @Override public void clearCachedAuthorizationInfo(PrincipalCollection principals) { super.clearCachedAuthorizationInfo(principals); } @Override public void clearCachedAuthenticationInfo(PrincipalCollection principals) { super.clearCachedAuthenticationInfo(principals); } @Override public void clearCache(PrincipalCollection principals) { super.clearCache(principals); } public void clearAllCachedAuthorizationInfo() { getAuthorizationCache().clear(); } public void clearAllCachedAuthenticationInfo() { getAuthenticationCache().clear(); } public void clearAllCache() { clearAllCachedAuthenticationInfo(); clearAllCachedAuthorizationInfo(); } }
上面的 doGetAuthorizationInfo 方法,会在权限认证也就是访问控制时,被回调,而 doGetAuthenticationInfo 方法会在登录认证时被回调,返回的 AuthenticationInfo类型的对象,会和用户登录时输入的 用户名和密码(加密之后的)进行比较,相同则登录成功,反之则登录失败。
 
4. Authorization 授权子系统(访问控制)
上一节中我们已经介绍了如何获得用户所拥有的权限,在需要判断用户是否有某权限或者角色时,会自动回调方法 doGetAuthorizationInfo 来获得用户的角色和权限,我们只需要在 该方法中从Realm也就是数据库表中获得相关信息。我们先看一下shiro是如何表示角色和权限的,这一点比较重要:
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String no = (String)principals.getPrimaryPrincipal(); User user = userService.getUserByNo(no); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); authorizationInfo.setRoles(userService.findRolesByUserId(user.getId())); authorizationInfo.setStringPermissions(userService.findPermissionsByUserId(user.getId())); return authorizationInfo; }
我们看到 doGetAuthorizationInfo 方法中使用了 SimpleAuthorizationInfo 类封装 Role 和 Permission.
public class SimpleAuthorizationInfo implements AuthorizationInfo { protected Set<String> roles; protected Set<String> stringPermissions; protected Set<Permission> objectPermissions; public SimpleAuthorizationInfo() { }
我们看到,roles 和 stringPermissions 都是 String 类型的 Set, 也就是说,它们都是使用字符串来表示你拥有某个角色或者拥有某个权限的。
 
1) 两种访问控制方式:
SimpleAuthorizationInfo 封装了角色和权限,其实这也说明了实现“访问控制”两种方式:一是 “基于角色的访问控制”;而是“基于资源的访问控制”。所谓的访问控制,是指对于某个资源,当前用户是否有访问的权限。基于角色的访问控制是一种比较粗粒度的访问控制方式,只要你具有了某个或某几个角色,那么你就可以访问某资源。而基于资源的访问控制,是判断你针对该资源是否有某权限,有才能访问,粒度更细,你是否有某权限,可以根据你有哪些角色,然后改角色有哪些权限来判断的,当然也可以不引入角色的概念,直接判断你是否拥有某些权限。当然两种访问方式可以单独使用,也可以混合使用。比如对于比较简单的权限控制,你可以仅仅只使用基于角色的访问控制,仅仅引入角色表,不需要权限表都可以。混合使用是指,你可以同时要求用户具有某角色并且具有某些权限,才能访问某资源。所以shiro的权限控制时极其灵活的(当然也可以不引入角色表,仅仅引入权限表)。
 
2)权限的字符串表示方式
上面说到 角色 和 权限 都是使用字符串来表示的,其实 shiro 提供了一套比较强大有点复杂的权限字符串表示格式(分为:分割的三个部分):
“资源:操作:对象实例ID” 表示:对那个资源的哪个实例可以进行哪些操作,支持通配符。
多个操作需要使用 “,” 逗号分割,而 “*” 放在三个位置上,分别表示:任意资源,任意操作,任意实例。
比如:"user:delete:1" 就表示 对user表的id等于1对应的数据或者对象,可以进行删除操作。其实资源表现实现可以是对象,其实最终是对应到数据库表中的记录。
在比如:"user:update,delete" 就表示 对user表(的任意实例)进行更新和删除操作。"user:update,delete" 其实就等价于 “user:update,delete:*”
所以 shiro 的访问控制可以控制到具体实例,或者说具体哪条数据库记录,也可以在表级别控制。如果省略掉 对象实例ID部分,就是在表级别控制。
 
3)权限相关表的设计
1> 如果对于简单的情况,可以只使用“基于角色的访问控制”粗粒度方式,不涉及到权限,仅仅只通过判断是否有某角色来判断访问控制,那么就只需要增加一个角色表(roles) 和 一个角色(roles)和用户(user)的多对多的一个中间表——用户角色表(user_role)。
2> 如果仅仅使用权限来控制访问,那么就可以仅仅只增加一个权限表(priv)和一个用户和权限的多对多的一个中间表——用户权限表(user_priv).
3> 如果既要用到角色,又要用到权限(权限根据角色推算出来),那么就要增加:角色表,用户角色表,权限表,角色权限表。
4> 其实还有一种情况:就是角色和权限没有关系,那么就可以增加:角色表,用户角色表,权限表,用户权限表。不过这种方式不同符合常规。
 
5. Cryptography 加密子系统
shiro提供了很完备而且十分易用的加密解密功能。该子系统分为两个部分:一是基于hash的单向加密算法;二是基于经典加密解密算法,密码是可以解密的出明文的;一般而言,对于登录用户的密码的加密都是采用单向的hash加密算法,因为如果密码可以被解密的话,一旦数据库被攻破了,那么所有用户的密码就都可以被解密成明文;但是单向的hash加密算法,没有这样的风险。单向的hash加密算法,就算你获得了数据库的中保存的密码密文,知道了密文对应的salt,甚至知道了使用的是什么hash算法,你都无法反向推算出密码的明文!因为hash是单向的,它没有对应的反向推算算法(也就是没有解密方法)。那么知道了密文,你是无法反推出密码明文的。这也是单向hash加密算法的妙处。
 
1> 直接使用  Sha256Hash/Md5Hash 等类,比如:
String sha256 = new Sha256Hash("admin", "11d23ccf28fc1e8cbab8fea97f101fc1d", 2).toString();
根据Sha256Hash的构造函数,"admin" 为需要加密的密码明文,"11d23ccf28fc1e8cbab8fea97f101fc1d" 为加密需要的salt, 2 是迭代次数,也就是hash次数。最后调用 .toString() 就获得了密文。很简单。
2> 使用 Sha256Hash/Md5Hash 等类 父类 SimpleHash ,比如:
sha1 = new SimpleHash("sha-256", "admin", "11d23ccf28fc1e8cbab8fea97f101fc1d", 2).toString();
3) 密码加密和密码验证
注册时一般涉及到密码加密,登录时涉及到密码验证。通过上面介绍的 加密算法,完全可以自己实现密码加密和密码验证。但是其实shiro也提供了相应的类:
DefaultPasswordService 和 HashedCredentialsMatcher。虽然提供了,其实  DefaultPasswordService 卵用都没有,因为他没有提供获取或者设置 salt 的方法,而 salt 是我们需要存入数据库的。所以密码加密我们是不使用 DefaultPasswordService 的,而是根据前面的介绍自己写。至于密码验证我们应该继承 HashedCredentialsMatcher,然后重写它的 doCredentialsMatch() 方法即可:
 
public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher { private Cache<String, AtomicInteger> passwordRetryCache; public RetryLimitHashedCredentialsMatcher(CacheManager cacheManager) { passwordRetryCache = cacheManager.getCache("passwordRetryCache"); } @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { String username = (String)token.getPrincipal(); AtomicInteger retryCount = passwordRetryCache.get(username); if(retryCount == null) { retryCount = new AtomicInteger(0); passwordRetryCache.put(username, retryCount); } if(retryCount.incrementAndGet() > 5) { throw new ExcessiveAttemptsException("超过了尝试登录的次数,您的账户已经被锁定。"); } boolean matches = super.doCredentialsMatch(token, info); if(matches) { passwordRetryCache.remove(username); } return matches; } }
 
super.doCredentialsMatch(token, info)调用了父类的方法:
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { Object tokenHashedCredentials = hashProvidedCredentials(token, info); Object accountCredentials = getCredentials(info); return equals(tokenHashedCredentials, accountCredentials); }
 
protected Object hashProvidedCredentials(AuthenticationToken token, AuthenticationInfo info) { Object salt = null; if (info instanceof SaltedAuthenticationInfo) { salt = ((SaltedAuthenticationInfo) info).getCredentialsSalt(); } else { //retain 1.0 backwards compatibility: if (isHashSalted()) { salt = getSalt(token); } } return hashProvidedCredentials(token.getCredentials(), salt, getHashIterations()); }
 
我们看到 AuthenticationToken token 加密时需要的 salt 来自于 AuthenticationInfo info:
salt = ((SaltedAuthenticationInfo) info).getCredentialsSalt();
AuthenticationToken token 是登录时页面传过来的用户名,明文密码等参数,AuthenticationInfo info却是从数据库中获得的用户名密码密文,salt等参数。equals(tokenHashedCredentials, accountCredentials); 验证: 明文密码使用相同的salt加密之后,获得的密文是否和数据库中的密码密文一致。一致,则密码验证通过。
 
6. Session Management会话管理子系统
shiro中session的最大不同时,它可以使用再非web环境中。对于JavaSE的环境也可以使用session的功能,因为他实现了一套不依赖于web容器的session机制。shiro提供了三个默认的实现:
1> DefaultSessionManager: DefaultSecurityManager使用的默认实现,用于JavaSE环境;
2> ServletContainerSessionManager: DefaultWebSecurityManager使用的默认实现,用于web环境,其直接使用Servlet容器的会话;
3> DefaultWebSessionManager: 用于web环境的实现,可以替代ServletContainerSessionManager,自己维护会话,直接替代Servlet容器的会话管理;
在web环境中默认使用的是 ServletContainerSessionManager,其使用的就是Servlet容器的会话,这一点,我们可以从它的源码中可以看出来:
 
public class ServletContainerSessionManager implements WebSessionManager {
//TODO - read session timeout value from web.xml public ServletContainerSessionManager() { } public Session start(SessionContext context) throws AuthorizationException { return createSession(context); } public Session getSession(SessionKey key) throws SessionException { if (!WebUtils.isHttp(key)) { String msg = "SessionKey must be an HTTP compatible implementation."; throw new IllegalArgumentException(msg); } HttpServletRequest request = WebUtils.getHttpRequest(key); Session session = null; HttpSession httpSession = request.getSession(false); if (httpSession != null) { session = createSession(httpSession, request.getRemoteHost()); } return session; } protected Session createSession(SessionContext sessionContext) throws AuthorizationException { if (!WebUtils.isHttp(sessionContext)) { String msg = "SessionContext must be an HTTP compatible implementation."; throw new IllegalArgumentException(msg); } HttpServletRequest request = WebUtils.getHttpRequest(sessionContext); HttpSession httpSession = request.getSession(); String host = getHost(sessionContext); return createSession(httpSession, host); } public boolean isServletContainerSessions() { return true; } }
 
DefaultWebSecurityManager默认使用的是ServletContainerSessionManager:
 
public class DefaultWebSecurityManager extends DefaultSecurityManager implements WebSecurityManager { private static final Logger log = LoggerFactory.getLogger(DefaultWebSecurityManager.class); @Deprecated public static final String HTTP_SESSION_MODE = "http"; @Deprecated public static final String NATIVE_SESSION_MODE = "native"; @Deprecated private String sessionMode; public DefaultWebSecurityManager() { super(); ((DefaultSubjectDAO) this.subjectDAO).setSessionStorageEvaluator(new DefaultWebSessionStorageEvaluator()); this.sessionMode = HTTP_SESSION_MODE; setSubjectFactory(new DefaultWebSubjectFactory()); setRememberMeManager(new CookieRememberMeManager()); setSessionManager(new ServletContainerSessionManager()); }
 
从 setSessionManager(new ServletContainerSessionManager()); 看到答案。
 
 
 
 
1. 配置
首先需要在web.xml中专门负责接入shiro的filter:
 
<!-- shiro 安全过滤器 --> <filter> <filter-name>shiroFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <async-supported>true</async-supported> <init-param> <param-name>targetFilterLifecycle</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
并且需要放在所有filter中靠前的位置,比如需要放在siteMesh的过滤器之前。
DelegatingFilterProxy 表示这是一个代理filter,它会将实际的工作,交给spring配置文件中 id="shiroFilter" 的bean来处理:
public class DelegatingFilterProxy extends GenericFilterBean { private String contextAttribute; private WebApplicationContext webApplicationContext; private String targetBeanName; private boolean targetFilterLifecycle = false; private volatile Filter delegate; private final Object delegateMonitor = new Object(); @Override protected void initFilterBean() throws ServletException { synchronized (this.delegateMonitor) { if (this.delegate == null) { if (this.targetBeanName == null) { this.targetBeanName = getFilterName(); } WebApplicationContext wac = findWebApplicationContext(); if (wac != null) { this.delegate = initDelegate(wac); } } } }
 
public abstract class GenericFilterBean implements Filter, BeanNameAware, EnvironmentAware, ServletContextAware, InitializingBean, DisposableBean { @Override public final void init(FilterConfig filterConfig) throws ServletException { Assert.notNull(filterConfig, "FilterConfig must not be null"); if (logger.isDebugEnabled()) { logger.debug("Initializing filter ‘" + filterConfig.getFilterName() + "‘"); } this.filterConfig = filterConfig; try { PropertyValues pvs = new FilterConfigPropertyValues(filterConfig, this.requiredProperties); BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this); ResourceLoader resourceLoader = new ServletContextResourceLoader(filterConfig.getServletContext()); bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.environment)); initBeanWrapper(bw); bw.setPropertyValues(pvs, true); } catch (BeansException ex) { String msg = "Failed to set bean properties on filter ‘" + filterConfig.getFilterName() + "‘: " + ex.getMessage(); logger.error(msg, ex); throw new NestedServletException(msg, ex); } initFilterBean(); if (logger.isDebugEnabled()) { logger.debug("Filter ‘" + filterConfig.getFilterName() + "‘ configured successfully"); } }
initFilterBean();
Filter 接口的 init 方法调用 initFilterBean(), 而该方法在子类中进行实现,它先获得 this.targetBeanName = getFilterName(); bean的名称,也就是id,然后对其进行初始化:this.delegate = initDelegate(wac); 其实就是从bean工厂中根据bean的名称找到bean.
 
protected Filter initDelegate(WebApplicationContext wac) throws ServletException { Filter delegate = wac.getBean(getTargetBeanName(), Filter.class); if (isTargetFilterLifecycle()) { delegate.init(getFilterConfig()); } return delegate; }
 
而 shiroFilter在spring中的配置如下:
 
<!-- Shiro的Web过滤器 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login"/> <property name="successUrl" value="/"/> <property name="unauthorizedUrl" value="/unauthorized"/> <property name="filters"> <util:map> <entry key="authc" value-ref="passThruAuthenticationFilter"/> </util:map> </property> <property name="filterChainDefinitions"> <value> /reg/** = anon <!-- 注册相关 --> /login = authc /logout = logout /authenticated = authc /loginController = anon /js/** = anon /css/** = anon /img/** = anon /html/** = anon /font-awesome/** = anon /** = user </value> </property> </bean>
 
上面的shiroFilter的配置又引出了 securityManager 和 shiro 的filter机制和他自带的一些filter.
 
2. securityManager 级相关配置
在上一篇文章 Java 权限框架 Shiro 实战一:理论基础 中我们知道securityManager是shiro的顶层对象,它管理和调用其它所有子系统,负责系统的安全。我们知道shiro有两个类型的securityManager:一个是JavaSE环境,默认是DefaultSecurityManager;一个是web环境,默认是DefaultWebSecurityManager。所以我们web环境肯定应该使用后者。我们从顶层对象一层一层向下配置。先看securityManager如何配置:
<!-- 相当于调用SecurityUtils.setSecurityManager(securityManager) --> <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean"> <property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager"/> <property name="arguments" ref="securityManager"/> </bean>
上面的配置相当于调用SecurityUtils.setSecurityManager(securityManager) ,来注入了下面配置的 securityManager(DefaultWebSecurityManager) :
<!-- 安全管理器 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="userRealm"/> <property name="cacheManager" ref="cacheManager"/> <property name="rememberMeManager" ref="rememberMeManager"/> </bean>
它默认使用的session管理器是 ServletContainerSessionManager,所以上面没有配置,所以就使用默认值。配置了就会覆盖下面的默认值:
 
public DefaultWebSecurityManager() { super(); ((DefaultSubjectDAO) this.subjectDAO).setSessionStorageEvaluator(new DefaultWebSessionStorageEvaluator()); this.sessionMode = HTTP_SESSION_MODE; setSubjectFactory(new DefaultWebSubjectFactory()); setRememberMeManager(new CookieRememberMeManager()); setSessionManager(new ServletContainerSessionManager()); }
 
显然 securityManager 最重要的工作就是用户登录认证和获得用户的权限等相关信息,所以 realm 是其最重要的依赖:
<!-- Realm实现 --> <bean id="userRealm" class="com.ems.shiro.UserRealm"> <property name="credentialsMatcher" ref="credentialsMatcher"/> <property name="cachingEnabled" value="false"/> </bean>
 
UserRealm 继承  AuthorizingRealm 显然是为了获取权限信息,对用户进行访问控制;继承AuthenticatingRealm显然是为了获得用户的认证信息,对用户进行认证。而 credentialsMatcher 就是 AuthenticatingRealm 使用来进行密码验证的依赖的组件:
public abstract class AuthenticatingRealm extends CachingRealm implements Initializable { private CredentialsMatcher credentialsMatcher;
再看其credentialsMatcher bean的配置:
 
<!-- 凭证匹配器(验证登录密码是否正确) --> <bean id="credentialsMatcher" class="com.ems.shiro.RetryLimitHashedCredentialsMatcher"> <constructor-arg ref="cacheManager"/> <property name="hashAlgorithmName" value="SHA-256"/> <property name="hashIterations" value="2"/> <property name="storedCredentialsHexEncoded" value="true"/> </bean>
 
配置就是 hash加密的相关参数:hash算法,hash迭代次数等。到这里 shiro 登录验证的配置就完了。至于获取用户信息和用户的权限的信息,都在userRealm中实现了:
 
public class UserRealm extends AuthorizingRealm { @Autowired private UserService userService; @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String userName = (String)principals.getPrimaryPrincipal(); User user = userService.getUserByUserName (userName ); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); authorizationInfo.setRoles(userService.findRolesByUserId(user.getId())); authorizationInfo.setStringPermissions(userService.findPermissionsByUserId(user.getId())); return authorizationInfo; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String userName = (String)token.getPrincipal(); User user = userService.getUserByUserName(userName); if(user == null) { throw new UnknownAccountException();//没找到账户 } if(user.getLocked() == 0) { throw new LockedAccountException(); //帐号锁定 } if(user.getLocked() == 2){ throw new AuthenticationException("account was inactive"); } SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( user.getUserName(), user.getPassword(), // 密码 ByteSource.Util.bytes(user.getCredentialsSalt()), // salt getName() // realm name ); return authenticationInfo; }
 
securityManager会在需要的时候回调上面 的 doGetAuthorizationInfo 和 doGetAuthenticationInfo 方法,从realm中获得登录认证信息和用户权限信息。至于 rememberMeManager 主要是实现使用cookie表示我已经登录过了,下次不需要重新登录,这一个功能,也就是“记住我”登录过这一功能:
 
<!-- rememberMe管理器 --> <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager"> <!-- rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)--> <property name="cipherKey" value="#{T(org.apache.shiro.codec.Base64).decode(‘9FvVhtFLUs0KnA3Kprsdyg==‘)}"/> <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="2592000"/><!-- 30天 --> </bean>
 
还有cacheManager的配置:
 
<!--ehcache--> <bean id="ehcacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"> <property name="configLocation" value="classpath:ehcache/ehcache.xml"/> </bean> <bean id="springCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager"> <property name="cacheManager" ref="ehcacheManager"/> </bean> <!-- 缓存管理器 --> <bean id="cacheManager" class="com.ems.shiro.SpringCacheManagerWrapper"> <property name="cacheManager" ref="springCacheManager"/> </bean>
 
使用的是 EhCache.
3. Shiro 的filter机制和自带的filter
Shiro的filter是基于Servlet的Filter接口实现的。我们通过Shiro提供的form登录filter:FormAuthenticationFilter 和 ShiroFilter 看看其实现:
继承中的每一层都实现了一些功能:
1> NameableFilter:实现给filter取名的功能(Allows a filter to be named via JavaBeans-compatible)
public abstract class NameableFilter extends AbstractFilter implements Nameable { private String name;
2> OncePerRequestFilter : 保证对于同一个request,fiter只执行一次(Filter base class that guarantees to be just executed once per request)
public abstract class OncePerRequestFilter extends NameableFilter {
3> AdviceFilter: SpringMVC风格的过滤器(就是preHandle, postHandle,afterCompletion 三接口的过滤器)
 
/**
 * A Servlet Filter that enables AOP-style &quot;around&quot; advice for a ServletRequest via
* preHandle(javax.servlet.ServletRequest, javax.servlet.ServletResponse),
 * postHandle(javax.servlet.ServletRequest, javax.servlet.ServletResponse),
 * and afterCompletion(javax.servlet.ServletRequest, javax.servlet.ServletResponse, Exception)hooks.
 */
public abstract class AdviceFilter extends OncePerRequestFilter { protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { return true; } @SuppressWarnings({"UnusedDeclaration"}) protected void postHandle(ServletRequest request, ServletResponse response) throws Exception { } @SuppressWarnings({"UnusedDeclaration"}) public void afterCompletion(ServletRequest request, ServletResponse response, Exception exception) throws Exception { }
 
4> PathMatchingFilter:该过滤器仅仅处理指定的路径(比如上面的配置:/js/** = anon,表示对 /js/ 目录和其子目录的请求,交给anon过滤器处理)
public abstract class PathMatchingFilter extends AdviceFilter implements PathConfigProcessor {
5> AccessControlFilter: 实现提供对资源的访问控制,没有权限时,重定向到登录页面,登录之后跳转到原来的那个页面
public abstract class AccessControlFilter extends PathMatchingFilter {
6> AuthenticationFilter: 实现对访问用户的认证要求,也就是必须登录了才能访问
public abstract class AuthenticationFilter extends AccessControlFilter {
7> AuthenticatingFilter: 实现判断用户是否有权限访问某资源。
public abstract class AuthenticatingFilter extends AuthenticationFilter {
8> FormAuthenticationFilter:shiro提供的用于实现用户登录功能,如果我们打算自己实现登录,那么我们应用 PassThruAuthenticationFilter 来替代
public class FormAuthenticationFilter extends AuthenticatingFilter {
9> PassThruAuthenticationFilter : 用于我们自己在controller中实现登录逻辑时替代FormAuthenticationFilter
 
public class PassThruAuthenticationFilter extends AuthenticationFilter { protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { if (isLoginRequest(request, response)) { return true; } else { saveRequestAndRedirectToLogin(request, response); return false; } } }
 
10> Shiro 自带的filter:
Shiro自身提供了很多的默认filter 来供我们使用,主要分为两种:一是 登录认证相关的filter;一是权限访问控制相关的filter;
登录认证相关的filter有:
1)filter名称: anon, 实现类org.apache.shiro.web.filter.authc.AnonymousFilter,主要用于静态资源的访问,表示无需登录就可以访问;
2)filter名称: authc, 实现类org.apache.shiro.web.filter.authc.FormAuthenticationFilter,主要用于表单登录,没有登录则跳转登录url;
3)filter名称: user, 实现类org.apache.shiro.web.filter.authc.UserFilter,主要用于要求用户已经登录或者通过“记住我”功能登录了也行。
4)filter名称: logout, 实现类org.apache.shiro.web.filter.authc.LogoutFilter,主要用于用户登出
5)filter名称: authcBasic,authc的简化形式,略。
权限访问控制相关的filter有:
1)filter名称: roles, 实现类org.apache.shiro.web.filter.authc.RolesAuthorizationFilter,主要用于验证用户必须拥有某角色,才能继续访问;
2)filter名称: perms, 实现类org.apache.shiro.web.filter.authc.PermissionsAuthorizationFilter,主要用于验证用户必须拥有某权限,才能继续访问;
3)filter名称: ssl, 实现类org.apache.shiro.web.filter.authc.SslFilter,主要用于要求访问协议是https才能访问,不然跳转到https的443短裤;
4)filter名称: port rest noSessionCreation,略。
我们上面的shiroFilter的配置中,已经使用过了上面这些自带的filter:
 
/reg/** = anon <!-- 注册相关 --> /login = authc /logout = logout /authenticated = authc /loginController = anon /js/** = anon /css/** = anon /img/** = anon /html/** = anon /font-awesome/** = anon /** = user
 
我们看到 /reg/** 注册相关的,/js/**静态资源都是使用的 anon匿名过滤器,不要求用户已经登录就可以访问。
/** = user 放在最后是要求除了上面那些 url 之外的访问路径,都需要登录认证过或者通过记住我登录认证过。因为路径比较是从上面开始列出来的先开始比较的,匹配了就走该过滤器,不会继续下面的过滤器了。
4. shiro的权限标签
Shiro提供了相应的权限标签,用来实现根据用户的角色和权限来显示它相应的菜单和按钮。首先需要导入shiro标签库:
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
标签库的定义位于:shiro-web.jar 包中的META-INF/shiro.tld文件中:
技术分享
 shiro.tld
其中最重要的标签是关于角色和权限的:
<shiro:hasAnyRoles name="student,teacher"></shiro:hasAnyRoles>
<shiro:hasPermission name="user:delete"></shiro:hashPermission>
其它还有关于登录与否的标签:
<shiro:guest></shiro:guest> 未登录可以显示的信息;
<shiro:user></shiro:user> 用户已经登录或者通过记住我登录后显示的信息;
<shiro:authenticated></shiro:authenticated> 必须是实际登录,不是通过记住我登录的
其它标签参考 shiro.tld文件。
shiro标签使用示例:
技术分享
 View Code
效果是根据用户拥有的角色,来显示左侧有哪些菜单项。
5. shiro 权限注解的使用
shiro对权限的控制,除了前面给出的在 shiroFilter这个bean中配置的过滤器:
 
<property name="filterChainDefinitions"> <value> /reg/** = anon <!-- 注册相关 --> /login = authc /logout = logout /loginController = anon /js/** = anon /css/** = anon /img/** = anon /html/** = anon /font-awesome/** = anon /** = user </value> </property>
 
之外,最重要的就是使用注解的方式来进行访问控制的实现了。shiro权限注解可以达到方法级别的细腻控制,可以控制具有某些权限或者某些角色的用户才能访问某个方法(某个url)。先要开启shiro权限注解功能,开启方法参见文档:http://shiro.apache.org/spring.html
Here is how to enable these annotations. Just add these two bean definitions to applicationContext.xml:
 
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> <!-- Enable Shiro Annotations for Spring-configured beans. Only run after --> <!-- the lifecycleBeanProcessor has run: --> <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>
 
开启shiro权限注解的方法二:
<aop:config /> <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager"/> </bean>
<aop:config /> 表示开启spring注解,而 DefaultAdvisorAutoProxyCreator 表示会自动创建代理。但是二者最好不要同时使用。
AuthorizationAttributeSourceAdvisor 通过其依赖的 securityManager 来获取用户的角色和权限信息,进而可以进行权限判断。
支持的shiro注解有:
 
@SuppressWarnings({"unchecked"}) public class AuthorizationAttributeSourceAdvisor extends StaticMethodMatcherPointcutAdvisor { private static final Logger log = LoggerFactory.getLogger(AuthorizationAttributeSourceAdvisor.class); private static final Class<? extends Annotation>[] AUTHZ_ANNOTATION_CLASSES = new Class[] { RequiresPermissions.class, RequiresRoles.class, RequiresUser.class, RequiresGuest.class, RequiresAuthentication.class }; protected SecurityManager securityManager = null;
    public AuthorizationAttributeSourceAdvisor() {
        setAdvice(new AopAllianceAnnotationsAuthorizingMethodInterceptor());
    }
 
RequiresPermissions, RequiresRoles, RequiresUser, RequiresGuest, RequiresAuthentication
主要是通过: AopAllianceAnnotationsAuthorizingMethodInterceptor 类来实现的:
 
public class AopAllianceAnnotationsAuthorizingMethodInterceptor extends AnnotationsAuthorizingMethodInterceptor implements MethodInterceptor{ public AopAllianceAnnotationsAuthorizingMethodInterceptor() { List<AuthorizingAnnotationMethodInterceptor> interceptors = new ArrayList<AuthorizingAnnotationMethodInterceptor>(5); AnnotationResolver resolver = new SpringAnnotationResolver(); interceptors.add(new RoleAnnotationMethodInterceptor(resolver)); interceptors.add(new PermissionAnnotationMethodInterceptor(resolver)); interceptors.add(new AuthenticatedAnnotationMethodInterceptor(resolver)); interceptors.add(new UserAnnotationMethodInterceptor(resolver)); interceptors.add(new GuestAnnotationMethodInterceptor(resolver)); setMethodInterceptors(interceptors); }
 
上面注入了注解的拦截器实现。具体的拦截判断权限过程实现如下:
 
public class RoleAnnotationHandler extends AuthorizingAnnotationHandler { public RoleAnnotationHandler() { super(RequiresRoles.class); } public void assertAuthorized(Annotation a) throws AuthorizationException { if (!(a instanceof RequiresRoles)) return; RequiresRoles rrAnnotation = (RequiresRoles) a; String[] roles = rrAnnotation.value(); if (roles.length == 1) { getSubject().checkRole(roles[0]); return; } if (Logical.AND.equals(rrAnnotation.logical())) { getSubject().checkRoles(Arrays.asList(roles)); return; } if (Logical.OR.equals(rrAnnotation.logical())) { boolean hasAtLeastOneRole = false; for (String role : roles) if (getSubject().hasRole(role)) hasAtLeastOneRole = true; if (!hasAtLeastOneRole) getSubject().checkRole(roles[0]); } } }
 
主要是上面的方法 assertAuthorized(Annotation a) 中来实现对用户是否拥有某些角色进行判断的。其实还是很简单的。
shiro权限注解使用方法如下所示:
 
@RequiresPermissions(value={"user:update", "user:select"}, logical= Logical.AND) @RequestMapping(value="/modifyPassword", method=RequestMethod.POST) @ResponseBody public Map<String, String> modifyPassword(String oldPassword, String newPassword, HttpSession session) { Map<String, String> map = new HashMap<>(); if(oldPassword == null || newPassword == null || newPassword.length() < 8 || newPassword.length() > 32){ map.put("result", "error"); map.put("msg", "密码必须在8到20位之间"); return map; } User user = (User)session.getAttribute(ConstantConfig.LONGIN_USER); if(user != null){ PasswordHelper ph = new PasswordHelper(); if(!ph.checkPasswordAndEncryptPassword(oldPassword, user)){ // 判断输入的 oldPassword是否正确 map.put("result", "error"); map.put("msg", "密码错误"); return map; }else{ user.setPassword(newPassword); ph.encryptPassword(user); int result = this.userService.updateUserById(user); if(result > 0){ map.put("result", "ok"); map.put("msg", "密码修改成功,请重新登录"); }else{ map.put("result", "error"); map.put("msg", "密码修改失败"); } return map; } } return map; }
 
@RequiresPermissions(value={"user:update", "user:select"}, logical= Logical.AND)
表示必须有 对 user 表的同时拥有 查询和更新权限,才能修改密码。

Java安全框架shiro

标签:proc   mesh   类的方法   名称   ons   ace   rda   attribute   man   

原文地址:http://www.cnblogs.com/Lichuntao/p/6653449.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!