Shrio安全框架

为什么要用Shiro?

1.项目的密码是否可以明文存储?

2.项目中的资源和功能,是否任意访客不用登录都可以访问?

3.项目中的资源和功能,是否任意用户都有权限可以访问?

综上所述,我们需要对用户的操作进行一个安全校验,而Shrio就是一个安全框架,帮助我们做了这些功能。

介绍

Apache Shiro™ 是一个功能强大且易于使用的 Java 安全框架,用于执行身份验证授权加密会话管理

基本功能:

  • 身份验证:有时称为“登录”,这是证明用户就是他们所说的身份的行为。
  • 授权:访问控制的过程,即确定“谁”有权访问“什么”。
  • 会话管理:即使在非Web或EJB应用程序中,也可以管理用户特定的会话。
  • 密码:使用密码算法保持数据安全,同时仍然易于使用。

其他功能:

  • Web支持:Shiro的Web支持API可帮助轻松保护Web应用程序。
  • 缓存:确保安全操作保持快速有效。
  • “记住我”:在整个会话中记住用户的身份,因此他们仅在必要时登录。

架构

高层概述

Shiro主要有三个核心,SubjectSecurityManagerRealm

  • Subject:代表当前访问程序的主体,可以是用户也可以是程序,系统调用Subject的API对主体进行认证、授权
  • SecurityManager:安全管理器,它管理着所有的Subject以及其他安全配置,主体的认证和授权实际是通过它进行
  • Realm:领域,相当于数据源,SecurityManager通过Realm读取主体的认证、授权相关数据

详细架构

  • Subject:代表当前访问程序的主体,可以是用户也可以是程序,系统调用Subject的API对主体进行认证、授权
  • SecurityManager:安全管理器,它管理着所有的Subject以及其他安全配置,主体的认证和授权实际是通过它进行
  • Realm:领域,相当于数据源,SecurityManager通过Realm读取主体的认证、授权相关数据
  • Authenticator:登录验证的组件,当用户尝试登录时,该逻辑由Authenticator执行,Authenticator知道如何与存储用户信息的Realm交互,从Realm获取数据验证用户的身份
  • Authorizer:决定用户是否有权限访问,它与Authenticator一样,从Realm获取数据验证权限
  • SessionManager:创建和管理用户session的生命周期
  • CacheManager:CacheManager创建和管理其他Shiro组件使用的Cache实例生命周期。因为这些数据很少改变,放入Cache可以提高性能
  • Cryptography:Shiro提供了加密API,可以进行加密解密,比如MD5、SH1

RBAC模型

RBAC:Role Base Access Controller基于角色的访问控制,Shiro的基础原理就是采用安全管理模型

模型中有三个主体:用户、角色、权限

  • 每个用户可以有多个角色、每个角色有多个用户
  • 每个角色可以有多个权限、每个权限有多个角色

角色对用户和权限做了一个解耦

权限规则

常用的权限标识:【资源 : 操作】

# ":" :作为分割符,分割资源和操作
# "," :作为多个权限的分割符
1.user:add, user:query, user:update, user:delete

# "*":通配符,表示所有资源
2.user:*, *:add

#对于user资源的所有操作
- user:*

#对于所有资源的query操作
- *:query

3.*:代表一切资源

4.细节:
  * user:* 可以匹配【user:xx,user:xx:xxx】; *:query只可以匹配【xx:query】,不可以匹配【xx:xx:query】
	*【user:insert, user:update, user:delete】可以简写为【"user:insert,update,delete"】,注意要加双引号

实例权限标识别:【资源 : 操作 : 实例】

# 对用户1可以update和delete
user:update:1 , user:delete:1
# 等同于上方
"user:update,delete:1"

URL路径匹配规则

格式是: “url=拦截器[参数],拦截器[参数]”

格式:url=拦截器[参数],拦截器[参数]
url支持Ant风格模式,Ant路径通配符支持?、*、**,通配符匹配不包括"/"
?:匹配一个字符,如 /admin?,匹配/admin1,不匹配/admin和/admin/
*:匹配零个或多个字符,如/admin*,匹配/admin123,/admin,不匹配/admin/123
**:匹配零个或多个路径,如/admin/**,匹配/admin/123
url采取第一次匹配优先的方式,所以对于匹配范围大的路径要放在后面

默认拦截器

Shiro 内置了很多默认的拦截器,比如身份验证、授权等相关的。默认拦截器可以参考org.apache.shiro.web.filter.mgt.DefaultFilter中的枚举拦截器:

public enum DefaultFilter {
   anon(AnonymousFilter.class),
   authc(FormAuthenticationFilter.class),
   authcBasic(BasicHttpAuthenticationFilter.class),
   logout(LogoutFilter.class),
   noSessionCreation(NoSessionCreationFilter.class),
   perms(PermissionsAuthorizationFilter.class),
   port(PortFilter.class),
   rest(HttpMethodPermissionFilter.class),
   roles(RolesAuthorizationFilter.class),
   ssl(SslFilter.class),
   user(UserFilter.class);

身份认证相关的

img

授权相关的

img

Shiro入门

一个简单的JavaSE程序了解Shiro的API使用

pom.xml

 <dependencies>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.4.0</version>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.26</version>
        </dependency>

        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>

        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.2</version>
        </dependency>
</dependencies>

编写ini

shiro支持在配置文件中读取数据,ini文件放在classpath路径下,当然,实际开发中并不会使用ini文件

# 定义用户信息
# 格式:用户=密码,角色1,角色2
[users]
zhangsan = 123,admin
lisi = 456,manager,clerk
wangwu = 789,clerk

# 定义
# 格式:角色=权限1,权限2
[roles]
admin = *
clerk = user:query,user:detail:query
manager = user:*

# 以下这部分,JAVASE测试没用,JAVAEE测试的话可以使用 
[main]
#没有身份认证时,跳转的地址
shiro.loginUrl = /user/login
#角色或者权限校验不同时,跳转的地址
shiro.unauthorizedUrl = /author/error
#登出后的跳转地址
shiro.redirectUrl = /user/login

[urls]
# 格式:路径 = 访问方式(anno匿名/authc验证) 范围大的要往后放,因为/** 放在最前面,一旦匹配到就会执行操作了
# anon:访问指定的路径,不需要身份验证,默认就是anno
# authc:访问指定的路径,需要身份验证,如果没有认证,会强制转发到上面配置的loginUrl上
# logout:登出,只需要配置路径,不需要在Controller去编写
# perms["user:all"]:用户需要该权限才能访问,如果没有,会强制转发到上面配置的unauthorizedUrl上
# roles["admin"]:用户需要是该角色才可以访问,如果没有,会强制转发到上面配置的unauthorizedUrl上
/user/login = anon

/user/all = authc,perms["user:all"]

#退出 不用定义请求
/user/logout = logout


#其余路径都需要身份验证
/** = authc

3.Java程序

主要方法:

subject.isAuthenticated():获取用户的登录状态

subject.login(token):用户登录

subject.hasRole("admin"):用户是否属于"admin"角色

sub.isPremitted("user:add"):用户是否有"user:Add"权限

sub.logout():用户退出,清除所有信息

/**
 * @User: dingjn
 * @Desc: Shiro测试类
 */
public class Shiro {

    public static void main(String[] args) {
        //读取.ini文件,创建Factory,通过它创建SecurityManager
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");

        //shiro核心 他管理所有的Subject,所有的安全操作都是和它交互的
        SecurityManager securityManager = factory.getInstance();

        //将securityManager托管到SecurityUtils工具类中,之后就不必再管他
        SecurityUtils.setSecurityManager(securityManager);

        //通过工具类创建Subject,可以执行Shiro相关操作,它是当前用户,每个请求独享的
        Subject subject = SecurityUtils.getSubject();

        //身份认证,获取当前用户的登录状态,是否已经登录(ops:从session中同步信息)
        System.out.println(subject.isAuthenticated());

        if (!subject.isAuthenticated()) {
            //参数为Token令牌,设置用户名和密码
            AuthenticationToken token = new UsernamePasswordToken("lisi", "456");
            //通过subject的login登录,
            //如果查询不到用户会抛出UnknownAccountException
            //如果用户的密码不正确会抛出IncorrectCredentialsException
            try {
                subject.login(token);
            } catch (UnknownAccountException e) {
                System.out.println("用户不存在:" + token.getPrincipal());
                return;
            } catch (IncorrectCredentialsException e) {
                System.out.println("密码错误:" + token.getPrincipal());
                return;
            } catch (LockedAccountException e) {
                System.out.println("账户冻结:" + token.getPrincipal());
            } catch (AuthenticationException e) {
                System.out.println("其他认证异常:" + e.getMessage());
            }
        }

        //是否有admin角色
        System.out.println(subject.hasRole("admin"));

        if (subject.hasRole("admin")) {
            System.out.println("Hello,管理员");
        } else {
            System.out.println("Hello,普通用户");
        }

        //是否有权限 如果没有UnauthorizedException
        System.out.println(subject.isPermitted("user:add"));

        if (subject.isPermitted("user:add")) {
            //正常访问
        } else {
            System.out.println("Sorry,您没有权限访问");
        }


        //登出:身份信息、登录状态信息、角色信息、权限信息、会话信息 全部抹除
        subject.logout();

        System.out.println(subject.isAuthenticated());

        System.out.println(subject.getPrincipal());
    }

}

Spring集成Shiro

pom.xml

<properties>
        <spring.version>5.2.5.RELEASE</spring.version>
    </properties>
    <dependencies>
        <!-- Spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jms</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.4.0</version>
        </dependency>

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-web</artifactId>
            <version>1.4.0</version>
        </dependency>

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>

        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.4</version>
        </dependency>

        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>2.0.4</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.19</version>
        </dependency>

        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>jsp-api</artifactId>
            <version>2.2</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
        </dependency>
    </dependencies>

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <!--1. 启动Spring容器-->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:applicationContext.xml</param-value>
    </context-param>

    <!--1.配置DispatcherServlet前端控制器,用于接收请求、拦截请求、分发请求给处理器-->
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring-mvc.xml</param-value>
        </init-param>
        <!--值越小,启动的越早-->
        <load-on-startup>1</load-on-startup>
    </servlet>
    <!--
     url-pattern / 和 /*的区别:
            / :匹配所有的请求,不匹配jsp页面
            /*:匹配所有的请求,包括jsp页面
    -->
    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <!--2.字符编码过滤器-->
    <filter>
        <filter-name>encoding</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <!--设置编码格式-->
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <!--是否允许上面的编码覆盖已经存在编码方式-->
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>false</param-value>
        </init-param>
    </filter>
    <!--
              /:不会匹配jsp页面,jsp还是会乱码
              /*:也会匹配jsp页面
      -->
    <filter-mapping>
        <filter-name>encoding</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>


    <!--shiro 过滤器
       1.接受所有请求,通过请求路径,识别是否需要安全校验
       2.在当前线程中绑定一个subject和SecurityManager,供SecurityUtils使用
		DelegatingFilterProxy是Filter的一个代理对象,默认情况下,Spring会到IOC容器中查找和<filter-name>对应的bean,也可以通过targetName定义name
   -->
    <!-- shiro的filter-->
    <filter>
        <filter-name>shiroFilter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
        <init-param>
            <param-name>targetFilterLifecycle</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>

    <!-- shiro的filter-mapping-->
    <filter-mapping>
        <filter-name>shiroFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>

spring-mvc.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/mvc
        https://www.springframework.org/schema/mvc/spring-mvc.xsd">

  	<!--一定要引入spring-shiro.xml-->
    <import resource="spring-shiro.xml"></import>

    <!--1.扫描controller包-->
    <context:component-scan base-package="com.djn.controller"/>

    <!--2.让SpringMVC不处理静态资源-->
    <mvc:default-servlet-handler/>

    <!--3.注解驱动 就可以使用@RequestMapping注解,如果使该注解生效
        必须配置HandlerMapping和HandlerAdapter实例
        该配置自动完成了以上两个实例的注入
    -->
    <mvc:annotation-driven/>

    <!--4.视图解析器 解析DispatcherServlet传的视图名解析为完整的视图名并返回给它-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <!--前缀    最后面的/一定要写!!!!!!!!!!!!!!!!!!!!!-->
        <property name="prefix" value="/WEB_INF/jsp/"></property>
        <!--后缀-->
        <property name="suffix" value=".jsp"/>
    </bean>

</beans>

applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">

    <import resource="spring-shiro.xml"></import>

</beans>

spring-shiro.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-4.2.xsd">


    <import resource="spring-dao.xml"></import>

    <!-- 对应于web.xml中配置的那个shiroFilter -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <!-- Shiro的核心安全接口,这个属性是必须的 -->
        <property name="securityManager" ref="securityManager"/>
        <!-- 没有身份认证时,跳转的地址,非必须的属性,默认会自动寻找Web工程根目录下的"/login.jsp"页面 -->
        <property name="loginUrl" value="/login.jsp"/>
        <!-- 登录成功后要跳转的连接(本例中此属性用不到,因为登录成功后的处理逻辑在LoginController里硬编码) -->
        <!-- <property name="successUrl" value="/" ></property> -->
        <!--用户权限不足或者没有角色使,强制转发到该url-->
        <property name="unauthorizedUrl" value="/error/unauthorized"/>
        <!--
						后面就不会在这里配置了
            配置每个路径需要认证才能访问
            以及需要访问的权限
            格式:url :规则
            url支持Ant风格模式,Ant路径通配符支持?、*、**,通配符匹配不包括"/"
            ?:匹配一个字符,如 /admin?,匹配/admin1,不匹配/admin和/admin/
            *:匹配零个或多个字符,如/admin*,匹配/admin123,/admin,不匹配/admin/123
            **:匹配零个或多个路径,如/admin/**,匹配/admin/123
            url采取第一次匹配优先的方式,所以对于匹配范围大的路径要放在后面
        -->
        <property name="filterChainDefinitions">
            <value>
                /login=anon
                /login.jsp=anon
              	<!--退出 不需要再去写方法了-->
                /logout=logout
                /** = authc
            </value>
        </property>

    </bean>


    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"></bean>
    <bean id="authorizationAttributeSourceAdvisor"
          class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
        <property name="securityManager" ref="securityManager"></property>
    </bean>
    <bean id="defaultAdvisorAutoProxyCreator"
          class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator">
        <property name="proxyTargetClass" value="true"></property>
    </bean>
    <!-- 数据库保存的密码是使用MD5算法加密的,所以这里需要配置一个密码匹配对象 -->
    <bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.Md5CredentialsMatcher"></bean>

    <!-- 缓存管理 -->
    <bean id="shiroCacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"></bean>

    <!--
    使用Shiro自带的JdbcRealm类
    指定密码匹配所需要用到的加密对象
    指定存储用户、角色、权限许可的数据源及相关查询语句
    -->
    <bean id="jdbcRealm" class="com.djn.MyRealm">
        <!--d-->
        <property name="credentialsMatcher">
            <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
                <property name="hashAlgorithmName" value="MD5"></property>
                <!--加密次数-->
                <property name="hashIterations" value="1024"></property>
            </bean>
        </property>
    </bean>

    <!--配置一个bean,该bean实际是一个map-->
    <bean id="filterChainDefinitionMap" factory-bean="shiroConfig" factory-method="beanfilterChainDefinitionMap"></bean>

    <bean id="shiroConfig" class="com.djn.ShiroConfig"></bean>

    <!-- Shiro安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="jdbcRealm"></property>
        <property name="cacheManager" ref="shiroCacheManager"></property>
        <property name="sessionManager" ref="sessionManager"></property>
        <property name="rememberMeManager" ref="rememberMeManager"></property>
    </bean>

    <!-- 定义RememberMe功能的程序管理类 -->
    <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
        <!-- 定义在进行RememberMe功能实现的时候所需要使用到的Cookie的处理类 -->
        <property name="cookie" ref="rememberMeCookie"/>
    </bean>
    <!-- 配置需要向Cookie中保存数据的配置模版(RememberMe) -->
    <bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
        <!-- 设置Cookie在浏览器中保存内容的名字,由用户自己来设置 -->
        <constructor-arg value="TEST-RememberMe"/>
        <!-- 保证该系统不会受到跨域的脚本操作供给 -->
        <property name="httpOnly" value="true"/>
        <!-- 定义Cookie的过期时间为一小时 -->
        <property name="maxAge" value="100"/>
    </bean>

    <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
        <property name="sessionIdUrlRewritingEnabled" value="false"></property>
    </bean>

    <!-- Shiro的注解配置一定要放在spring-mvc中 -->

</beans>

认证

认证流程:

1.构建SecurityManager环境

2.获取当前的Subject,调用SecurityUtils.getSubject();

2.判断当前用户有没有认证,subject.subject.isAuthenticated()

3.如果没有认证,就将用户的用户名和密码存入Token中

​ 1)用户名和密码由前端传递过来

​ 2)接受参数

4.调用subject.log(token)进行登录认证

5.从Realm中获取用户信息进行验证,并返回,自定义Realm

​ 1) 创建一个类继承AuthenticatingRealm

​ 2)重写doGetAuthenticationInfo方法

6.由Shiro完成用户信息的对比

1.jsp

<html>
<head>
    <title>Title</title>
</head>
<body>
<form action="/shiro/login" method="post">
    <input type="text" name="username"/>
    <input type="text" name="password"/>
    <input type="submit" value="登录">
</form>
</body>
</html>

2.controller

@Controller
@RequestMapping("/shiro")
public class ShiroController {
    @PostMapping("/login")
    public String login(String username, String password) {
        Subject subject = SecurityUtils.getSubject();
        if (!subject.isAuthenticated()) {
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
            token.setRememberMe(true);
            subject.login(token);
        }
        return "redirect:/index.jsp";
    }
}

自定义Realm

为什么要自定义Realm呢?

之前我们的用户数据都存储在 ini 文件中,有默认的IniRealm去加载,而我们的实际项目中肯定不会把用户、角色、权限数据存储在文件中,一是不利于管理,二是不灵活

1.如果只需要认证的话,继承AuthenticatingRealm

2.如果需要认证和授权的话,继承AuthorizingRealm

public class MyAuthorizingRealm extends AuthorizingRealm {
    /**
     * 授权.
     */
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }
    /**
     * 认证.
     */
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        return null;
    }
}

3.使用MD5加密,这样前端传递过来的明文密码就会自动加密,当然,也可以不配置,在代码中登录的时候手动加密,在shiro.xml中的Realm中配置

 <bean id="myRealm" class="com.djn.MyRealm">
            <!--d-->
            <property name="credentialsMatcher">
                <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
                  	<!--value也可以使用SH1等加密方式-->
                    <property name="hashAlgorithmName" value="MD5"></property>
                    <!--加密次数-->
                    <property name="hashIterations" value="1024"></property>
                </bean>
            </property>
</bean>

完整代码:

public class MyRealm extends AuthorizingRealm {
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
      	//获取用户名
        String username = (String) token.getPrincipal();
      	//去数据库查找信息,这里是模拟登录错误
        if ("unknow".equals(username)) {
            throw new UnknownAccountException("用户名不存在");
        }
        if ("moster".equals(username)) {
            throw new IncorrectCredentialsException("账户锁定");
        }
      	//模拟数据库中的密码
        String credentials = "";
        if ("admin".equals(username)) {
            //数据库存储的就是加了盐值的,在注册的时候 登录的时候还要加盐
            credentials = "038bdaf98f2037b31f1e75b5b4c9b26e";
        } else if ("user".equals(username)) {
            credentials = "098d2c478e9c11555ce2823231e02ec1";
        }
        //盐值 使用唯一字符串 保证相同的明文密码加密后也不一样
        ByteSource bytes = ByteSource.Util.bytes(username);
        //Shiro底层会将token中的密码与它做比对 getName:调用父类方法,获取当前Realm
        return new SimpleAuthenticationInfo(username, credentials, bytes, getName());
    }

    /**
     * 授权
     */
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

        //获取用户信息
        Object principal = principals.getPrimaryPrincipal();
      
        //去数据库查询用户的角色,这里是模拟两个角色
        HashSet<String> set = new HashSet<String>();

        set.add("user");
        if ("admin".equals(principal)) {
            set.add("admin");
        }
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(set);
        return info;
    }
}

授权

Shiro 支持三种方式的授权:

  • 编程式:通过写if/else 授权代码块完成
  • 注解式:通过在执行的Java方法上放置相应的注解完成,没有权限将抛出相 应的异常(常用)
  • JSP/GSP 标签:在JSP/GSP 页面通过相应的标签完成

授权流程:

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表示 授权失败。

Shiro标签

Shiro提供了JSTL标签用于JSP权限控制,如根据用户权限不同显示不同的按钮

1.导入shiro

<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>

2.使用

# <shiro:principal/>显示当前用户名
Welcome:<shiro:principal/> <br>
<!--<shiro:hasRole name="admin"> 具有admin角色才会显示该内容-->
<shiro:hasRole name="admin">
    <a href="/admin.jsp">admin.page</a><br>
</shiro:hasRole><br>

<!--<shiro:hasRole name="user"> 具有user角色才会显示该内容-->
<shiro:hasRole name="user">
    <a href="/user.jsp"> user.page</a><br>
</shiro:hasRole><br>

<a href="/shiro/logout">logout</a><br>
</body>

权限注解

通过注解来控制权限,可以在Service方法或者Controller方法上添加

注意:如果Service方法有事务@Transcation注解,那就不能添加权限注解,因为@Transcation它会代理一个对象,不能代理一个代理,会发生类型转换异常

@RequieRoles({"admin","user"}):表示需要admin和user角色

@RequirePermissions({"user:add","user,update"}):表示需要user:add和user:update权限

@RequireAuthentication:表示当前用户已经通过了身份认证(不包括记住我)

@RequireUser:表示当前用户已经身份认证或者记住我

@RequireGest:表示当前用户没有身份认证或者记住我,即游客身份

@RequestMapping("/test_annotation")
@RequirePermissions({"user:add","user,update"})
public String testAnnotation() {
    System.out.println("testAnnotation");
    return "redirect:/index.jsp";
}

异常处理

这里对异常进行一个统一处理

用户名匹配不到会抛出UnknownAccountException;

密码错误会抛出IncorrectCredentialsException;

没有权限会抛出UnauthorizedException

@ControllerAdvice
public class BaseExceptionHandler {

    @ExceptionHandler({UnknownAccountException.class})
    public String handle(UnknownAccountException e) {
        System.out.println("【登录认证】-----> 用户名错误");
        return "redirect:/user/login";
    }

    @ExceptionHandler({IncorrectCredentialsException.class})
    public String handle2(IncorrectCredentialsException e) {
        System.out.println("【登录认证】-----> 密码错误");
        return "redirect:/user/login";
    }
  
  	@ExceptionHandler({	UnauthorizedException.class})
    public String handle2(UnauthorizedException e) {
        System.out.println("【访问资源】-----> 权限不足");
        return "redirect:/user/login";
    }
  
  

}

url配置

之前的url配置是在shiro.xml的 中配置,但是如果配置太多就不好管理,可以将配置单独一个类

1.shiro.xml

将之前的filterChainDefinitions去掉,替换为filterChainDefinitionMap

<!-- 对应于web.xml中配置的那个shiroFilter -->
    <bean id="abc" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <!-- Shiro的核心安全接口,这个属性是必须的 -->
        <property name="securityManager" ref="securityManager"/>
        <!-- 没有身份认证时,跳转的地址,非必须的属性,默认会自动寻找Web工程根目录下的"/login.jsp"页面 -->
        <property name="loginUrl" value="/login.jsp"/>
        <!-- 登录成功后要跳转的连接(本例中此属性用不到,因为登录成功后的处理逻辑在LoginController里硬编码) -->
        <!-- <property name="successUrl" value="/" ></property> -->
        <!--用户权限不足或者没有角色使,强制转发到该url-->
        <property name="unauthorizedUrl" value="/error/unauthorized"/>
      
      	<!--替换为一个Bean-->
        <property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"></property>
    </bean>

 <!--配置一个bean,该bean实际是一个map-->
    <bean id="filterChainDefinitionMap" factory-bean="shiroConfig" factory-method="beanfilterChainDefinitionMap"></bean>

    <bean id="shiroConfig" class="com.djn.ShiroConfig"></bean>

2.ShiroConfig

/**
 * @Desc: Shiro统一配置
 */
public class ShiroConfig {

    public LinkedHashMap<String, String> beanfilterChainDefinitionMap() {
      	//注意这里必须是LinkedHashMap,保证有序
        LinkedHashMap<String, String> hashMap = new LinkedHashMap<String, String>();
      	//顺序要弄对,范围大的放在后面,因为会采取最先匹配原则
        hashMap.put("/login.jsp", "anon");
        hashMap.put("/shiro/logout", "logout");
        hashMap.put("/shiro/login", "anon");
        hashMap.put("/user.jsp", "roles[admin]");
        hashMap.put("/**", "authc");
        return hashMap;
    }
}

会话管理

Shiro提供了Session会话管理,不依赖底层容器,不管是JAVAEE环境还是JAVASE环境都可以使用,提供了会话持久化、失效/过期支持等

在Controller层还是使用HttpSession,在service层可以直接使用Shiro的session,更加方便

@Controller
@RequestMapping("/shiro")
public class ShiroController {

    @Resource
    TestService testService;

    @RequestMapping("/test_annotation")
    @RequiresRoles({"admin"})
    public String testAnnotation(HttpSession session) {
        session.setAttribute("key", "dingjn");
        testService.test();
        return "redirect:/index.jsp";
    }
}
@Service
public class TestService {

    public void test() {
        Session session = SecurityUtils.getSubject().getSession();
        Object key = session.getAttribute("key");
        System.out.println(key);
    }
}

记住我

Shiro提供了记住我的功能,当关闭了浏览器以后下次不需要验证,可以直接显示

1.设置路径的过滤器为user

user:当用户第一次开启了rememberMe以后,用户下一次访问如果登录状态还在(也就是Cookie还在)就不需要再次认证,这里的下一次访问指的是用户关闭浏览器后再次打开;比如网站的首页就可以设置为user

authc:无论用户有没有开启rememberMe,下一次访问都需要 重新认证,比如支付,下单就需要重新认证

public class ShiroConfig {

    public LinkedHashMap<String, String> beanfilterChainDefinitionMap() {
        LinkedHashMap<String, String> hashMap = new LinkedHashMap<String, String>();
        hashMap.put("/login.jsp", "anon");
        hashMap.put("/shiro/logout", "logout");
        hashMap.put("/shiro/login", "anon");
        hashMap.put("/user.jsp", "authc,roles[user]");
        hashMap.put("/admin.jsp", "authc,roles[admin]");
        hashMap.put("/index.jsp", "user"); //设置过滤器为user
        hashMap.put("/**", "authc");
        return hashMap;
    }
}

2.设置时间

我们以前实现记住我是通过设置Cookie的有效时间从而将Cookie持久化本地,Shiro也一样

 <!-- 配置需要向Cookie中保存数据的配置模版(RememberMe) -->
    <bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
        <!-- 设置Cookie在浏览器中保存内容的名字,由用户自己来设置 -->
        <constructor-arg value="TEST-RememberMe"/>
        <!-- 保证该系统不会受到跨域的脚本操作供给 -->
        <property name="httpOnly" value="true"/>
        <!-- 定义Cookie的过期时间为一小时 -->
        <property name="maxAge" value="100"/>
    </bean>

      <!-- 定义RememberMe功能的程序管理类 -->
    <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
        <!-- 定义在进行RememberMe功能实现的时候所需要使用到的Cookie的处理类 -->
        <property name="cookie" ref="rememberMeCookie"/>
    </bean>
    
<!--避免url携带jessionId-->
    <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
        <property name="sessionIdUrlRewritingEnabled" value="false"></property>
    </bean>
    
<!-- Shiro安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
      	<!--Realm配置-->
        <property name="realm" ref="jdbcRealm"></property>
      	<!--缓存配置-->
        <property name="cacheManager" ref="shiroCacheManager"></property>
      	<!--Session配置-->
        <property name="sessionManager" ref="sessionManager"></property>
      	<!--记住我配置-->
        <property name="rememberMeManager" ref="rememberMeManager"></property>
    </bean>

3.登录设置

在登录时将rememberMe设置为true,实际开发中根据前端传过来的checkbox值来判断

 UsernamePasswordToken token = new UsernamePasswordToken(username, password);
 token.setRememberMe(true);
 subject.login(token);

SpringBoot集成Shiro

Todo.

posted @ 2020-04-23 13:45  范特西-  阅读(601)  评论(0)    收藏  举报