上一篇我们了解到可以通过Spring的拦截器HandlerInceptorAdapter的preHandler进行简单的一些验证,这种是我们大部分使用的,但这种比较麻烦,我们要配置很多的inceptor,比较繁琐,不易维护。URL(资源)和权限表示方式不规范。
介绍:Shiro是apache的一个开源框架,是一个权限管理框架,主要包括认证、用户授权。
认证其实就是我们常用的username,password登录这些,但shiro还包括验证码、记住我这些,就是关于用户身份的验证。
授权就是验证你这个用户对这个操作有没有权限,比如我们现在都习惯用restful的请求格式,有这样一个URL:http://localhost:8080/project/user/edit/2;从上面看,是完成用户编辑修改的一个操作,如果这个用户没有改权限,直接通过浏览器输入这个请求,就会直接请求,当然通过拦截器也可以,但还是比较麻烦。 shiro可以直接完成
概念:
1、subject:主体,可以是用户、程序。 主体要访问系统,系统要对主体进行认证、授权。可以理解为上火车票上车,主体就是进站的人,检票的要对主体(进站)的验证
2、securityManager:安全管理器,主体进行认证和授权都是由SecurityManager进行,这是一个大的概念,比如上车检票是一个部门,什么部门,不清楚,假设检票部
3、authenicator:认证器,最外层的检查,比如火车站现在要什么(人、票、身份证)
4、authorizer:授权器。 进站了,还有授权,你是硬座、卧铺、硬卧,并不是拿一张票,什么都能坐,所以车厢都有查票的,授权你可以进了才能进
5、sessionManager:shiro自己的一套session管理器
6、sessionDao:通过sessionDao对sessionManager进行个性化管理
7、cache Manager:缓存管理器, 主要对session和认证数据进行缓存,结合ecache使用,如果我们每访问一个页面,都访问DB,验证用户名、密码,那性能肯定有影响,所以就需要缓存,这种缓存跟redis很相似
8、realm:域、领域,相当于数据源,通过realm对主体进行认证、授权
一、所需jar:(基于Maven)
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-quartz</artifactId>
<version>1.2.3</version>
</dependency>
每个包的含义就字面上就看出来了,就不解释了
二、配置
1、首先配置web.xml,shiro是基于filter来实现拦截,所以首先配filter
<!-- shiro filter相关配置 -->
<!--DelegatingFilterProxy通过代理模式,将spring容器的bean 与 filter关联 -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<!-- 设置true表示由servlet容器控制filter的生命周期 -->
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
<!-- 设置spring bean 与 该filter关联的bean,如果不设置,默认为filter-name -->
<init-param>
<param-name>targetBeanName</param-name>
<param-value>shiroFilter</param-value>
</init-param>
</filter>
2、接下来配置spring与shiro结合的相关配置,文件名自己起,我定义的application-shiro.xml
上面在web.xml中我们定义了一个targetBeanName属性,所以我们要在application-shiro.xml里配置id为shiroFilter的bean,在定义shiroFileter bean的时候,我们先定义个id为securityManager的bean,这个bean可以理解为一个核心控制器,它里面可以配自定义的realm,缓存等,先一步步来,
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="myRealm" />
</bean>
<!-- 自定义的realm主要包括两个方法:身份认证、授权 -->
<bean id="myRealm" class="com.think.realm.UserInfoRealm"></bean>
目前这个securityManager 我们就引用了一个自定义的realm,这个realm里面有两个方法,一个是认证、一个是授权。 我们只管写好,调用的事全是shiro帮我们完成
接下来就是shiroFilter的配置了
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<property name="loginUrl" value="/authen/login" />
<property name="successUrl" value="/" />
<property name="unauthorizedUrl" value="/refuse.jsp" />
<property name="filterChainDefinitions">
<value>
<!--对静态资源设置匿名访问-->
/images/** = anon
/js/** = anon
/styles/** = anon
<!-- 验证码,可匿名访问 -->
/validatecode.jsp = anon
authen/logout = logout
/** = authc
</value>
</property>
</bean>
属性介绍:
securityManager这个是必须的,这个是shrio的核心,我们的认证、授权都要经过他,类似Spring的DispatcherServlet
loginUrl:这个是认证失败或者没认证跳转的页面,默认是项目路径下的login.jsp。 但是要注意的是:如果在<property name = "filterChainDefinitions">中配置了 /** = authc,此处的value值要和你登录表单提交的action一致,也就是loginUrl 和处理登录的方法要一样,原因下面讲
successUrl:认证成功后,跳转的页面,根据你配置的,自动调转,如果没配,会跳转你上一次请求的页面。比如,你没认证,请求了 queryGoods这个requestMapping,认证成功后,自动请求上一个,即queryGoods
unauthorizedUrl:未授权,提示的页面
filterChainDefinitions:过滤器链的定义,就是对那些请求进行过滤,每个请求后面对应的值其实都是一个filter,按照就近原则,最先匹配那个,就使用那个
下面介绍下过滤器,有很多,我就说几个常用的
anon:表示可以匿名访问,就是无序登录认证就可以查看,例子:/images/** = anon 比如大多数中的js,css ,images 。
Filter类: org.apache.shiro.web.filter.authc.AnonymousFilter
authc:需要认证,才可以访问,没有参数。例子:/queryGoods = authc
Filter类:org.apache.shiro.web.filter.authc.FormAuthenticationFilter
roles:表示需要某种角色,可以操作。例子:/editGoods = roles[admin] 需要管理员角色才能修改,角色可以写多个,多个的时候,必须加引号,以逗号隔开 /editGoods = roles["admin,manager"] Filter类:org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
perms:需要某种权限的可以操作。例子 /editGoods = perms[goods:edit],同样可以多个,这个表示对goods有edit的权限可操作
Filter类:org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
user:通过记住我,可以直接访问 例子:/index.jsp = user,表示可以通过记住我,访问
logout:注销,帮我们自动消除session
其他还有几种,平时用的不多,有兴趣了再看!
三、流程
下面以登录为例子,通过源码分析下,shiro是怎么通过这些filter工作的
前端表单
<form action="<%=request.getContentPath()%>//authen/login">
username:<input name="username" /> <br>
password:<input name="password" /> <br>
<input type="submit" value="submit">
</form>
后台处理
@RequestMapping(value = "/login")
public String doLogin(HttpServletRequest request) {
String loginFailure = (String)request.getAttribute("shiroLoginFailure");
// 如果登录失败,跳到login.jsp
return "login";
}
spring相关配置就不列了,shiro的配置就是这些,我启动服务,打开index.jsp,由于我设置了 /** = authc,所以会拦截
拦截流程
1、我们知道authc这个Filter类是FormAuthenticationFilter,我们在里面的方法上打个断点看
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
if (isLoginRequest(request, response)) {
if (isLoginSubmission(request, response)) {
if (log.isTraceEnabled()) {
log.trace("Login submission detected. Attempting to execute login.");
}
return executeLogin(request, response);
} else {
if (log.isTraceEnabled()) {
log.trace("Login page view.");
}
//allow them to see the login page ;)
return true;
}
} else {
if (log.isTraceEnabled()) {
log.trace("Attempting to access a path which requires authentication. Forwarding to the " +
"Authentication url [" + getLoginUrl() + "]");
}
saveRequestAndRedirectToLogin(request, response);
return false;
}
}
这里面有几个if判断,首先判断你请求的是否是Login页面,他是根据你的loginUrl 与当前请求的request.getRequestUrl()来判断,根据现在情况,我请求的是/project,所以直接走else,最后返回了一个false。
所以我在上面shrio的loginUrl配置中设置的,表单提交地址要和loginUrl一样,不然外面的if判断就会直接else,返回false,你登录表单提交后,就又返回到登录页面
返回false后,框架会自动请求loginUrl的页面,这时候走的就是if (ifLoginRequest(rquest, response)),里面还嵌套了一个if 中的else部分,上面有个log.trace("Login page view."),这时候到login.jsp页面,填写完表单后,点提交,同样进来,这时候走if (isLoginSubmission(request, response)),去执行登录,executeLogin(request, response).
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
AuthenticationToken token = createToken(request, response);
if (token == null) {
String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " +
"must be created in order to execute a login attempt.";
throw new IllegalStateException(msg);
}
try {
Subject subject = getSubject(request, response);
subject.login(token);
return onLoginSuccess(token, subject, request, response);
} catch (AuthenticationException e) {
return onLoginFailure(token, e, request, response);
}
}
executeLogin首先根据用户名、密码构造一个token,然后去subject.login(token).
后面的代码就不帖了,直接到后面
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
if (token == null) {
throw new IllegalArgumentException("Method argumet (authentication token) cannot be null.");
}
log.trace("Authentication attempt received for token [{}]", token);
AuthenticationInfo info;
try {
info = doAuthenticate(token);
看到了doAuthenticate(token)这个方法
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,我们的realm里面有认证、授权方法,回去调用我们的realm中认证方法,这个方法一般我们是根据token的用户名,去查密码,返回用户名和DB中的密码,也就是下面代码中,需要的AuthenticationInfo
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
if (!realm.supports(token)) {
String msg = "Realm [" + realm + "] does not support authentication token [" +
token + "]. Please ensure that the appropriate Realm implementation is " +
"configured correctly or that the realm accepts AuthenticationTokens of this type.";
throw new UnsupportedTokenException(msg);
}
AuthenticationInfo info = realm.getAuthenticationInfo(token);
if (info == null) {
String msg = "Realm [" + realm + "] was unable to find account data for the " +
"submitted AuthenticationToken [" + token + "].";
throw new UnknownAccountException(msg);
}
return info;
}
调用了getAuthentication(token);先从缓存里面取,第一次肯定没有,然后doGetAuthenticationInfo(token); 这个就是我们自定义realm里面的方法,返回token中的用户名,已经DB中用户密码
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info = getCachedAuthenticationInfo(token);
if (info == null) {
//otherwise not cached, perform the lookup:
info = doGetAuthenticationInfo(token);
log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
if (token != null && info != null) {
cacheAuthenticationInfoIfPossible(token, info);
}
} else {
log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
}
if (info != null) {
assertCredentialsMatch(token, info);
} else {
log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
}
return info;
}
如果我们根据username没查到用户信息,返回null,上面代码中直接就抛出没有按账户异常,如果有if (info != null),先保存到Request里面,下面他就去验证密码。密码验证Ok后,再返回info
这时候认证Ok,会去标记已经认证,再访问其他页面的时候,subject里面就是已经认证通过。
同样,如果我们清除session,那么subject的认证状态就是false,这时他就会再让你去登录
下一节,说一下授权,本节主要是认证,存在的问题是:认证通过,他并不是每个Url都能请求,但目前没有限制,存在安全隐患。