上一篇我们了解到可以通过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>
接下来就是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;
- }
- }
所以我在上面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);
- }
- }
- 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都能请求,但目前没有限制,存在安全隐患。