权限验证框架Shiro学习(一)

shiro权限验证框架的学习

上一篇我们了解到可以通过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都能请求,但目前没有限制,存在安全隐患。

相关文章
相关标签/搜索