Shiro

Shiro简介:

官网:https://shiro.apache.org/

什么是Shiro:

Shiro是一个功能强大且易于使用的Java安全框架,它执行身份验证、授权、加密和会话管理。使用Shiro易于理解的API,您可以快速轻松地保护任何应用程序—从最小的移动应用程序到最大的web和企业应用程序。

  • Apache Shiro是一个Java 的安全(权限)框架。

  • Shiro可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境。

  • Shiro可以完成,认证,授权,加密,会话管理,Web集成,缓存等.

对比Spring Security,可能没有Spring Security做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的Shiro就足够了。

Shiro简介:

Apache Shiro是Java的一个安全框架。功能强大,使用简单的Java安全框架,它为开发人员提供一个直观而全面的认证,授权,加密及会话管理的解决方案。

实际上,Shiro的主要功能是管理应用程序中与安全相关的全部,同时尽可能支持多种实现方法。Shiro是建立在完善的接口驱动设计和面向对象原则之上的,支持各种自定义行为。Shiro提供的默认实现,使其能完成与其他安全框架同样的功能,这不也是我们一直努力想要得到的吗!

Apache Shiro相当简单,对比Spring Security,可能没有Spring Security做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的Shiro就足够了。对于它俩到底哪个好,这个不必纠结,能更简单的解决项目问题就好了。

Shiro可以做什么:

  • 验证用户身份
  • 用户访问控制,比如用户是否被赋予了某个角色;是否允许访问某些资源
  • 在任何环境都可以使用Session API,即使不是WEB项目或没有EJB容器
  • 事件响应(在身份验证,访问控制期间,或是session生命周期中)
  • 集成多种用户信息数据源
  • SSO-单点登陆
  • Remember Me,记住我
  • Shiro尝试在任何应用环境下实现这些功能,而不依赖其他框架、容器或应用服务器。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

有哪些功能:

在这里插å
¥å›¾ç‰‡æè¿°

  • Authentication: 身份认证、登录,验证用户是不是拥有相应的身份;
  • Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限,即判断用户能否进行什么操作,如:验证某个用户是否拥有某个角色,或者细粒度的验证某个用户对某个资源是否具有某个权限!
  • Session Manager: 会话管理,即用户登录后就是第-次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通的JavaSE环境,也可以是Web环境;
  • Cryptography: 加密,保护数据的安全性,如密码加密存储到数据库中,而不是明文存储;
  • Web Support: Web支持,可以非常容易的集成到Web环境;
  • Caching: 缓存,比如用户登录后,其用户信息,拥有的角色、权限不必每次去查,这样可以提高效率
  • Concurrency: Shiro支持多线程应用的并发验证,即,如在-个线程中开启另-一个线程,能把权限自动的传
    播过去
  • Testing:提供测试支持;
  • RunAs:允许一个用户假装为另-一个用户(如果他们允许)的身份进行访问;
  • Remember Me:记住我,这个是非常常见的功能,即一-次登录后, 下次再来的话不用登录了

Shiro核心架构:

Shiro可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境。Shiro可以帮助我们完成:认证、授权、加密、会话管理、与Web集成、缓存等。这不就是我们想要的嘛,而且Shiro的API也是非常简单;其基本功能点如下图所示:

img

**Authentication:**身份认证/登录,验证用户是不是拥有相应的身份,例如账号密码登陆;

**Authorization:**授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;

**Session Manager:**会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;

**Cryptography:**加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;

**Web Support:**Web支持,可以非常容易的集成到Web环境;

Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;

**Concurrency:**shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;

**Testing:**提供测试支持;

**Run As:**允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;

**Remember Me:**记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。

记住一点,Shiro不会去维护用户、维护权限;这些需要我们自己去设计提供;然后通过相应的接口注入给Shiro即可。

接下来我们分别从外部和内部来看看Shiro的架构,对于一个好的框架,从外部来看应该具有非常简单易于使用的API,且API契约明确;从内部来看的话,其应该有一个可扩展的架构,即非常容易插入用户自定义实现,因为任何框架都不能满足所有需求。

首先,我们从外部来看Shiro吧,即从应用程序角度的来观察如何使用Shiro完成工作。如下图:

img

可以看到:应用代码直接交互的对象是Subject,也就是说Shiro的对外API核心就是Subject;其每个API的含义:

**Subject:**主体,应用代码直接交互的对象是 Subject,也就是说 Shiro 的对外 API 核心就是 Subject , 代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者;

**SecurityManager:**安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器;

**Realm:**域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。

也就是说对于我们而言,最简单的一个Shiro应用:

1、应用代码通过Subject来进行认证和授权,而Subject又委托给SecurityManager;

2、我们需要给Shiro的SecurityManager注入Realm,从而让SecurityManager能得到合法的用户及其权限进行判断。

从以上也可以看出,Shiro不提供维护用户权限,而是通过Realm让开发人员自己注入。

接下来我们来从Shiro内部来看下Shiro的架构,如下图所示:

img

**Subject:**主体,可以看到主体可以是任何可以与应用交互的“用户”;

**SecurityManager:**相当于SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是Shiro的心脏;所有具体的交互都通过SecurityManager进行控制;它管理着所有Subject、且负责进行认证和授权、及会话、缓存的管理。

**Authenticator:**认证器,负责主体认证的,这是一个扩展点,如果用户觉得Shiro默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;

**Authrizer:**授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;

**Realm:**可以有1个或多个Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC实现,也可以是LDAP实现,或者内存实现等等;由用户提供;注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的Realm;

**SessionManager:**如果写过Servlet就应该知道Session的概念,Session呢需要有人去管理它的生命周期,这个组件就是SessionManager;而Shiro并不仅仅可以用在Web环境,也可以用在如普通的JavaSE环境、EJB等环境;所有呢,Shiro就抽象了一个自己的Session来管理主体与应用之间交互的数据;这样的话,比如我们在Web环境用,刚开始是一台Web服务器;接着又上了台EJB服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到Memcached服务器);

**SessionDAO:**DAO大家都用过,数据访问对象,用于会话的CRUD,比如我们想把Session保存到数据库,那么可以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把Session放到Memcached中,可以实现自己的Memcached SessionDAO;另外SessionDAO中可以使用Cache进行缓存,以提高性能;

**CacheManager:**缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能

**Cryptography:**密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密的。

到此Shiro架构及其组件就认识完了,接下来挨着学习Shiro的组件吧。

Shiro架构(外部):

从外部来看Shiro,即从应用程序角度来观察如何使用shiro完成工作:

在这里插å
¥å›¾ç‰‡æè¿°

  • subject: 应用代码直接交互的对象是Subject, 也就是说Shiro的对外API核心就是Subject, Subject代表了当前的用户,这个用户不-定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等,与Subject的所有交互都会委托给SecurityManager; Subject其实是一一个门面, SecurityManageer 才是
    实际的执行者
  • SecurityManager: 安全管理器,即所有与安全有关的操作都会与SercurityManager交互, 并且它管理着所有的Subject,可以看出它是Shiro的核心,它负责与Shiro的其他组件进行交互,它相当于SpringMVC的DispatcherServlet的角色
  • Realm: Shiro从Realm获取安全数据 (如用户,角色,权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较,来确定用户的身份是否合法;也需要从Realm得到用户相应的角色、权限,进行验证用户的操作是否能够进行,可以把Realm看DataSource;

Shiro架构(内部):

img

  • Subject: 任何可以与应用交互的用户;
  • Security Manager:相当于SpringMVC中的DispatcherSerlet; 是Shiro的心脏, 所有具体的交互都通过Security Manager进行控制,它管理者所有的Subject, 且负责进行认证,授权,会话,及缓存的管理。
  • Authenticator:负责Subject认证, 是-一个扩展点,可以自定义实现;可以使用认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
  • Authorizer:授权器,即访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中
    的那些功能;
  • Realm: 可以有-一个或者多个的realm, 可以认为是安全实体数据源,即用于获取安全实体的,可以用JDBC实现,也可以是内存实现等等,由用户提供;所以- -般在应用中都需要实现自己的realm
  • SessionManager:管理Session生 命周期的组件,而Shiro并不仅仅可以用在Web环境,也可以用在普通的JavaSE环境中
  • CacheManager: 缓存控制器,来管理如用户,角色,权限等缓存的;因为这些数据基本上很少改变,放到缓存中后可以提高访问的性能;
  • Cryptography:密码模块,Shiro 提高了一些常见的加密组件用于密码加密, 解密等。

===================================================================================================

在这里插å
¥å›¾ç‰‡æè¿°

Subject:

Subject即主体,外部应用与subject进行交互,subject记录了当前的操作用户,将用户的概念理解为当前操作的主体。外部程序通过subject进行认证授权,而subject是通过SecurityManager安全管理器进行认证授权

SecurityManager:

SecurityManager即安全管理器,对全部的subject进行安全管理,它是shiro的核心,负责对所有的subject进行安全管理。通过SecurityManager可以完成subject的认证、授权等,实质上SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等

SecurityManager是一个接口,继承了Authenticator, Authorizer, SessionManager这三个接口

Authenticator:

Authenticator即认证器,对用户身份进行认证,Authenticator是一个接口,shiro提供ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足大多数需求,也可以自定义认证器

Authorizer:

Authorizer即授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限

Realm:

Realm即领域,相当于datasource数据源,securityManager进行安全认证需要通过Realm获取用户权限数据,比如:如果用户身份数据在数据库那么realm就需要从数据库获取用户身份信息

不要把realm理解成只是从数据源取数据,在realm中还有认证授权校验的相关的代码

SessionManager:

sessionManager即会话管理,shiro框架定义了一套会话管理,它不依赖web容器的session,所以shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录

SessionDAO:

SessionDAO即会话dao,是对session会话操作的一套接口,比如要将session存储到数据库,可以通过jdbc将会话存储到数据库

CacheManager:

CacheManager即缓存管理,将用户权限数据存储在缓存,这样可以提高性能

Cryptography:

Cryptography即密码管理,shiro提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能。

Shiro中的认证:

什么是认证
身份认证,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确

三个概念:

Subject:

访问系统的用户,主体可以是用户、程序等,进行认证的都称为主体

Principal:

身份信息,是主体(subject)进行身份认证的标识,标识必须具有唯一性,如用户名、手机号、邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份(Primary Principal)

credential:

凭证信息,是只有主体自己知道的安全信息,如密码、证书等

认证的实现:

1、创建一个普通的maven项目,引入shiro的pom依赖

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

2、引入shiro配置文件shiro.ini,并加入以下配置

# 约定写法
[users]
# 用户名=密码
christy=123456
tide=654321

shiro的配置文件是一个.ini文件,类似于.txt文件

.ini文件经常用作某些软件的特定的配置文件,可以支持一些复杂的数据格式,shiro可以按照内部约定的某种格式读取配置文件中的数据

之所以提供这个配置文件是用来学习shiro时书写我们系统中相关的权限数据,从而减轻配置数据库并从数据库读取数据的压力,降低学习成本,提高学习效率

3、测试Java代码

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.subject.Subject;

public class ShiroAuthenticatorTest {
    public static void main(String[] args){
        // 1、创建安全管理器对象
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        // 2、给安全管理器设置realm
        securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
        // 3、给全局安全工具类SecurityUtils设置安全管理器
        SecurityUtils.setSecurityManager(securityManager);
        // 4、拿到当前的subject
        Subject subject = SecurityUtils.getSubject();
        // 5、创建令牌
        AuthenticationToken token = new UsernamePasswordToken("christy","123456");

        try {
            // 6、用户认证
            System.out.println("认证状态:"+subject.isAuthenticated());
            subject.login(token);
            System.out.println("认证状态:"+subject.isAuthenticated());
        } catch (UnknownAccountException e){
            e.printStackTrace();
            System.out.println("认证失败:用户不存在!");
        } catch (IncorrectCredentialsException e){
            e.printStackTrace();
            System.out.println("认证失败:密码不正确!");
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}

认证的几种状态

UnknownAccountException:用户名错误

IncorrectCredentialsException:密码错误

DisabledAccountException:账号被禁用

LockedAccountException:账号被锁定

ExcessiveAttemptsException:登录失败次数过多

ExpiredCredentialsException:凭证过期

Shiro认证的源码分析:

上面我们已经简单实现了shiro的认证,但是shiro内部认证的具体流程是怎么样的,这次我们通过追踪源码的方式具体分析一下。我们在认证处打上断点,点击debug模式运行,然后一步步运行到最后,中间经过的类我们都记录下来

在这里插å
¥å›¾ç‰‡æè¿°

在这里插å
¥å›¾ç‰‡æè¿°

在这里插å
¥å›¾ç‰‡æè¿°

在这里插å
¥å›¾ç‰‡æè¿°

在这里插å
¥å›¾ç‰‡æè¿°

在这里插å
¥å›¾ç‰‡æè¿°

在这里插å
¥å›¾ç‰‡æè¿°

在这里插å
¥å›¾ç‰‡æè¿°

在这里插å
¥å›¾ç‰‡æè¿°

在这里插å
¥å›¾ç‰‡æè¿°

在这里插å
¥å›¾ç‰‡æè¿°

在这里插å
¥å›¾ç‰‡æè¿°

至此啊,在SimpleAccountRealm中完成了用户名的认证。

那么密码呢?在哪里校验的呢?我们继续点击下一步,直到这里

在这里插å
¥å›¾ç‰‡æè¿°

我们看到这里断言密码是否匹配的方法,点进去

在这里插å
¥å›¾ç‰‡æè¿°

我们看到了这里拿的是我们输入的密码和根据token取出的用户中的密码做的比较来验证密码是否正确,这是系统帮我们完成的

上面我们说了用户的认证是在SimpleAccountRealm的doGetAuthenticationInfo的方法中完成的,而SimpleAccountRealm继承自AuthorizingRealm,而AuthorizingRealm中有一个抽象方法

protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection var1);

SimpleAccountRealm就是复写了AuthorizingRealm中的这个抽象方法实现的用户认证,所以后面我们需要自定义认证的时候我们就可以自定义一个realm继承自AuthorizingRealm来复写doGetAuthorizationInfo,在这个方法里面实现我们自己的认证逻辑

不仅认证,有意思的是AuthorizingRealm是继承自AuthenticatingRealm,而AuthenticatingRealm中有个抽象方法

protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken var1) throws AuthenticationException;

这个方法是实现用户授权的方法。

也就是说通过我们自定义realm继承AuthorizingRealm就可以同时复写认证和授权两个方法

Realm的继承关系如下

在这里插å
¥å›¾ç‰‡æè¿°

自定义Relam实现认证:

上面我们实现了简单的认证并且分析了认证的基本流程,通常情况下shiro的认证都是通过自定义relam来实现的

CustomerRealm:

首先我们编写自定义realm的代码:

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

/**
 * 自定义realm
 */
public class CustomerRealm extends AuthorizingRealm {
    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 在token中获取用户名
        String principal = (String) token.getPrincipal();
        System.out.println(principal);
        // 模拟根据身份信息从数据库查询
        if("christy".equals(principal)){
            // 参数说明:用户名 | 密码 | 当前realm的名字
            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal,"123456",this.getName());
            return simpleAuthenticationInfo;
        }

        return null;
    }
}

CustomerRealmAuthenticatorTest

import com.christy.shiro.realm.CustomerRealm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;

public class CustomerRealmAuthenticatorTest {
    public static void main(String[] args) {
        // 创建SecurityManager
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        // 设置自定义realm
        defaultSecurityManager.setRealm(new CustomerRealm());
        // 设置安全工具类
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        // 通过安全工具类获取subject
        Subject subject = SecurityUtils.getSubject();
        // 创建token
        UsernamePasswordToken token = new UsernamePasswordToken("christy", "123456");
        try {
            // 登录认证
            subject.login(token);
            System.out.println(subject.isAuthenticated());
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("用户名错误");
        } catch (IncorrectCredentialsException e) {
            e.printStackTrace();
            System.out.println("密码错误");
        }
    }
}

测试:

以上代码编写完成后,我们运行CustomerRealmAuthenticatorTest里面的main方法,执行结果如下

在这里插å
¥å›¾ç‰‡æè¿°

Shiro中的授权:

什么是授权:

授权可简单理解为who对what(which)进行How操作:

Who,即主体(Subject),主体需要访问系统中的资源。

What,即资源(Resource),如系统菜单、页面、按钮、类方法、系统商品信息等。资源包括资源类型和资源实例,比如商品信息为资源类型,类型为t01的商品为资源实例,编号为001的商品信息也属于资源实例。

How,权限/许可(Permission),规定了主体对资源的操作许可,权限离开资源没有意义,如用户查询权限、用户添加权限、某个类方法的调用权限、编号为001用户的修改权限等,通过权限可知主体对哪些资源都有哪些操作许可。

授权方式:

基于角色的访问控制:

RBAC基于角色的访问控制(Role-Based Access Control)是以角色为中心进行访问控制

if(subject.hasRole("admin")){
   //操作什么资源
}

基于资源的访问控制:

RBAC基于资源的访问控制(Resource-Based Access Control)是以资源为中心进行访问控制

if(subject.isPermission("user:update:01")){ //资源实例
  //对01用户进行修改
}
if(subject.isPermission("user:update:*")){  //资源类型
  //对01用户进行修改
}

权限字符串:

权限字符串的规则是:资源标识符:操作:资源实例标识符,意思是对哪个资源的哪个实例具有什么操作,“:”是资源/操作/实例的分割符,权限字符串也可以使用*通配符。

例子:

  • 用户创建权限:user:create,或user:create:*
  • 用户修改实例001的权限:user:update:001
  • 用户实例001的所有权限:user:*:001

权限的编码方式:

编程式:

Subject subject = SecurityUtils.getSubject();
if(subject.hasRole("admin")) {
	//有权限
} else {
	//无权限
}

注解式:

@RequiresRoles("admin")
public void hello() {
	//有权限
}

标签式:

JSP/GSP 标签:在JSP/GSP 页面通过相应的标签完成:
<shiro:hasRole name="admin">
	<!— 有权限—>
</shiro:hasRole>
注意: Thymeleaf 中使用shiro需要额外集成!

授权的实现:

我们基于上面MD5加密的例子进行修改

CustomerMD5Realm

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;

public class CustomerMD5Realm extends AuthorizingRealm {
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 从系统返回的身份信息集合中获取主身份信息(用户名)
        String primaryPrincipal = (String) principals.getPrimaryPrincipal();
        System.out.println("用户名: "+primaryPrincipal);

        //根据用户名获取当前用户的角色信息,以及权限信息
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

        //将数据库中查询角色信息赋值给权限对象
        simpleAuthorizationInfo.addRole("admin");
        simpleAuthorizationInfo.addRole("user");

        //将数据库中查询权限信息赋值个权限对象
        simpleAuthorizationInfo.addStringPermission("user:*:01");
        simpleAuthorizationInfo.addStringPermission("product:create");

        return simpleAuthorizationInfo;
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 从token中获取用户名
        String principal = (String) token.getPrincipal();
        if("christy".equals(principal)){
            /**
             * 用户名
             * 加密后的密码
             * 随机盐
             * 当前realm的名称
             */
            return new SimpleAuthenticationInfo(principal,
                    "41a4e25bcf1272844e38b19047dd68a0",
                    ByteSource.Util.bytes("1q2w3e"),
                    this.getName());
        }
        return null;
    }
}

CustomerMD5AuthenticatorTest

import com.christy.shiro.realm.CustomerMD5Realm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;

public class CustomerMD5AuthenticatorTest {
    public static void main(String[] args) {
        // 创建SecurityManager
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        // 设置自定义realm
        CustomerMD5Realm realm = new CustomerMD5Realm();
        // 为realm设置凭证匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        // 设置加密算法
        credentialsMatcher.setHashAlgorithmName("md5");
        // 设置hash次数
        credentialsMatcher.setHashIterations(1024);
        realm.setCredentialsMatcher(credentialsMatcher);
        defaultSecurityManager.setRealm(realm);
        // 设置安全工具类
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        // 通过安全工具类获取subject
        Subject subject = SecurityUtils.getSubject();
        // 创建token
        UsernamePasswordToken token = new UsernamePasswordToken("christy", "123456");
        try {
            // 登录认证
            subject.login(token);
            System.out.println("认证成功");  
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("用户名错误");
        } catch (IncorrectCredentialsException e) {
            e.printStackTrace();
            System.out.println("密码错误");
        }
        
        //授权
        if(subject.isAuthenticated()){
            //基于角色权限控制
            System.out.println(subject.hasRole("super"));

            //基于多角色权限控制(同时具有)
            System.out.println(subject.hasAllRoles(Arrays.asList("admin", "super")));

            //是否具有其中一个角色
            boolean[] booleans = subject.hasRoles(Arrays.asList("admin", "super", "user"));
            for (boolean aBoolean : booleans) {
                System.out.println(aBoolean);
            }
            
            System.out.println("==============================================");

            //基于权限字符串的访问控制  资源标识符:操作:资源类型
            System.out.println("权限:"+subject.isPermitted("user:update:01"));
            System.out.println("权限:"+subject.isPermitted("product:create:02"));

            //分别具有那些权限
            boolean[] permitted = subject.isPermitted("user:*:01", "order:*:10");
            for (boolean b : permitted) {
                System.out.println(b);
            }

            //同时具有哪些权限
            boolean permittedAll = subject.isPermittedAll("user:*:01", "product:create:01");
            System.out.println(permittedAll);
        }
    }
}

测试

用户名: christy
false
用户名: christy
用户名: christy
false
用户名: christy
用户名: christy
用户名: christy
true
false
true
==============================================
用户名: christy
权限:true
用户名: christy
权限:true
用户名: christy
用户名: christy
true
false
用户名: christy
用户名: christy
true

基于角色的授权:

持久层

# 用户表上面已经有了:t_user

/*Table structure for table `t_role` */

DROP TABLE IF EXISTS `t_role`;
CREATE TABLE `t_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(64) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;


/*Table structure for table `t_user_role` */

DROP TABLE IF EXISTS `t_user_role`;
CREATE TABLE `t_user_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(8) DEFAULT NULL,
  `role_id` int(8) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;

在这里插å
¥å›¾ç‰‡æè¿°

t_user中的数据是我们通过上面认证注册的用户

在这里插å
¥å›¾ç‰‡æè¿°

t_role角色表中有两种角色adminuser

在这里插å
¥å›¾ç‰‡æè¿°

我们为用户christy赋予了admin的权限,tide赋予了user的权限

视图层index.jsp

<%--解决页面乱码--%>
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport"
              content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>index</title>
    </head>

    <body>
        <h1>系统主页</h1>
        <a href="${pageContext.request.contextPath}/user/logout">退出用户</a>
        <ul>
            <%-- admin角色的用户能同时拥有用户管理和订单管理的权限,user角色的用户只拥有订单管理的权限 --%>
            <shiro:hasRole name="admin">
            <li>
                <a href="">用户管理</a>
            </li>
            </shiro:hasRole>
            
            <shiro:hasAnyRoles name="admin,user">
            <li><a href="">订单管理</a></li>
            </shiro:hasAnyRoles>
        </ul>
    </body>
</html>

实体类User.java与Role.java

package com.christy.shiro.model.entity;

import com.baomidou.mybatisplus.annotation.*;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

/**
 * @Author Christy
 * @DESC
 * @Date 2020/11/16 15:38
 **/
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("t_user")
@ApiModel("用户实体类")
public class User implements Serializable {
    /** 其他属性省略 **/

    @TableField(exist = false)
    private List<Role> roles = new ArrayList<>();
}
package com.christy.shiro.model.entity;

import com.baomidou.mybatisplus.annotation.*;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

/**
 * @Author Christy
 * @DESC
 * @Date 2020/11/16 15:38
 **/
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("t_role")
@ApiModel("角色实体类")
public class Role implements Serializable {
    /** 数据库中设置该字段自增时该注解不能少 **/
    @TableId(type = IdType.AUTO)
    @ApiModelProperty(name = "id", value = "ID 主键")
    private Integer id;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    @ApiModelProperty(name = "name", value = "角色名称")
    private String name;
}

Mapper层

package com.christy.shiro.model.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.christy.shiro.model.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

/**
 * @Author Christy
 * @DESC
 * @Date 2020/11/16 15:49
 **/
@Mapper
public interface UserMapper extends BaseMapper<User> {

    /**
     * 根据用户名查找用户
     * @author Christy
     * @date 2020/11/17 22:19
     * @param username
     * @return com.christy.mplus.model.entity.User
     */
    @Select("SELECT u.id,u.username,u.password,u.salt,u.age,u.email,u.address FROM t_user u WHERE u.username = #{username}")
    User findUserByUsername(String username);
}
package com.christy.shiro.model.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.christy.shiro.model.entity.Role;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

import java.util.List;

/**
 * @Author Christy
 * @DESC
 * @Date 2020/11/17 22:40
 **/
@Mapper
public interface RoleMapper extends BaseMapper<Role> {
    /**
     * 根据用户id查询用户的角色
     * @author Christy
     * @date 2020/11/17 22:42
     * @param userId
     * @return java.util.List<com.christy.mplus.model.entity.Role>
     */
    @Select("select r.id,r.name from t_role r left join t_user_role ur on ur.role_id = r.id where ur.user_id = #{userId}")
    List<Role> getRolesByUserId(Integer userId);
}

Service层

UserService不变,新建RoleService.javaRoleServiceImpl.java

package com.christy.shiro.service;

import com.christy.shiro.model.entity.Role;

import java.util.List;

public interface RoleService {
    /**
     * 根据用户id获取用户的角色集合
     * @param userId
     * @return
     */
    List<Role> getRolesByUserId(Integer userId);
}
package com.christy.shiro.service.impl;

import com.christy.shiro.model.entity.Role;
import com.christy.shiro.model.mapper.RoleMapper;
import com.christy.shiro.service.RoleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service("roleService")
public class RoleServiceImpl implements RoleService {
    @Autowired
    private RoleMapper roleMapper;

    @Override
    public List<Role> getRolesByUserId(Integer userId) {
        return roleMapper.getRolesByUserId(userId);
    }
}

Realm中实现授权

package com.christy.shiro.configuration.shiro.realm;

import com.christy.shiro.configuration.shiro.salt.CustomerByteSource;
import com.christy.shiro.model.entity.Role;
import com.christy.shiro.model.entity.User;
import com.christy.shiro.service.RoleService;
import com.christy.shiro.service.UserService;
import com.christy.shiro.utils.ApplicationContextUtil;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;

import java.util.List;

/**
 * 自定义realm
 */
public class CustomerRealm extends AuthorizingRealm {
    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 获取主身份信息
        String principal = (String) principals.getPrimaryPrincipal();
        // 根据主身份信息获取角色信息
        UserService userService = (UserService) ApplicationContextUtil.getBean("userService");
        User user = userService.findUserByUserName(principal);
        RoleService roleService = (RoleService) ApplicationContextUtil.getBean("roleService");
        List<Role> roles = roleService.getRolesByUserId(user.getId());
        if(!CollectionUtils.isEmpty(roles)){
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            roles.forEach(role -> {
                simpleAuthorizationInfo.addRole(role.getName());
            });
            return simpleAuthorizationInfo;
        }
        return null;
    }

    /** 认证代码省略 **/
}

重启项目测试

在这里插å
¥å›¾ç‰‡æè¿°

上图可以看到admin角色的用户登录系统后能够看到用户管理和订单管理,user角色的用户只能看到订单管理

基于权限的授权:

持久层

新增t_permissiont_role_permission

/*Table structure for table `t_permission` */

DROP TABLE IF EXISTS `t_permission`;
CREATE TABLE `t_permission` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(128) DEFAULT NULL,
  `url` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;


/*Table structure for table `t_role_permission` */

DROP TABLE IF EXISTS `t_role_permission`;
CREATE TABLE `t_role_permission` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `role_id` int(11) DEFAULT NULL,
  `permission_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;

在这里插å
¥å›¾ç‰‡æè¿°

在这里插å
¥å›¾ç‰‡æè¿°

视图层-index.jsp

<%--解决页面乱码--%>
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport"
              content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>index</title>
    </head>

    <body>
        <h1>系统主页</h1>
        <a href="${pageContext.request.contextPath}/user/logout">退出用户</a>
        <ul>
            <%-- admin角色的用户能同时拥有用户管理和订单管理的权限,user角色的用户只拥有订单管理的权限 --%>
            <shiro:hasRole name="admin">
            <li>
                <a href="">用户管理</a>
            </li>
            </shiro:hasRole>

            <%-- admin角色的用户对订单有增删改查的权限,user角色的用户只能查看订单 --%>
            <shiro:hasAnyRoles name="admin,user">
            <li>
                <a href="">订单管理</a>
                <ul>
                    <shiro:hasPermission name="order:add:*">
                        <li><a href="">新增</a></li>
                    </shiro:hasPermission>
                    <shiro:hasPermission name="order:del:*">
                        <li><a href="">删除</a></li>
                    </shiro:hasPermission>
                    <shiro:hasPermission name="order:upd:*">
                        <li><a href="">修改</a></li>
                    </shiro:hasPermission>
                    <shiro:hasPermission name="order:find:*">
                        <li><a href="">查询</a></li>
                    </shiro:hasPermission>

                </ul>
            </li>
            </shiro:hasAnyRoles>
        </ul>
    </body>
</html>

实体类Role.java与Permission.java

package com.christy.shiro.model.entity;

import com.baomidou.mybatisplus.annotation.*;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

/**
 * @Author Christy
 * @DESC
 * @Date 2020/11/16 15:38
 **/
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("t_role")
@ApiModel("角色实体类")
public class Role implements Serializable {
    /** 其他属性字段省略 **/

    @TableField(exist = false)
    private List<Permission> permissions = new ArrayList<>();
}
package com.christy.shiro.model.entity;

import com.baomidou.mybatisplus.annotation.*;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * @Author Christy
 * @DESC
 * @Date 2020/11/16 15:38
 **/
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("t_permission")
@ApiModel("权限实体类")
public class Permission implements Serializable {
    /** 数据库中设置该字段自增时该注解不能少 **/
    @TableId(type = IdType.AUTO)
    @ApiModelProperty(name = "id", value = "ID主键")
    private Integer id;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    @ApiModelProperty(name = "name", value = "权限名称")
    private String name;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    @ApiModelProperty(name = "url", value = "权限菜单URL")
    private String url;
}

Mapper层

package com.christy.shiro.model.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.christy.shiro.model.entity.Permission;
import com.christy.shiro.model.entity.Role;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

import java.util.List;

@Mapper
public interface PermissionMapper extends BaseMapper<Permission> {

    /**
     * 根据角色id查询权限
     * @author Christy
     * @date 2020/12/01 22:42
     * @param roleId
     * @return java.util.List<com.christy.mplus.model.entity.Permission>
     */
    @Select("select p.id,p.name,p.url from t_permission p left join t_role_permission rp on rp.permission_id = p.id where rp.role_id = #{roleId}")
    List<Permission> getPermissionsByRoleId(Integer roleId);
}

Service层

package com.christy.shiro.service;

import com.christy.shiro.model.entity.Permission;

import java.util.List;

public interface PermissionService {
    /**
     * 根据角色id获取权限集合
     * @param roleId
     * @return
     */
    List<Permission> getPermissionsByRoleId(Integer roleId);
}
package com.christy.shiro.service.impl;

import com.christy.shiro.model.entity.Permission;
import com.christy.shiro.model.mapper.PermissionMapper;
import com.christy.shiro.service.PermissionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service("permissionService")
public class PermissionServiceImpl implements PermissionService {
    @Autowired
    private PermissionMapper permissionMapper;

    @Override
    public List<Permission> getPermissionsByRoleId(Integer roleId) {
        return permissionMapper.getPermissionsByRoleId(roleId);
    }
}

Realm中实现授权

package com.christy.shiro.configuration.shiro.realm;

import com.christy.shiro.configuration.shiro.salt.CustomerByteSource;
import com.christy.shiro.model.entity.Permission;
import com.christy.shiro.model.entity.Role;
import com.christy.shiro.model.entity.User;
import com.christy.shiro.service.PermissionService;
import com.christy.shiro.service.RoleService;
import com.christy.shiro.service.UserService;
import com.christy.shiro.utils.ApplicationContextUtil;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;

import java.util.List;

/**
 * 自定义realm
 */
public class CustomerRealm extends AuthorizingRealm {
    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 获取主身份信息
        String principal = (String) principals.getPrimaryPrincipal();
        // 根据主身份信息获取角色信息
        UserService userService = (UserService) ApplicationContextUtil.getBean("userService");
        User user = userService.findUserByUserName(principal);

        RoleService roleService = (RoleService) ApplicationContextUtil.getBean("roleService");
        List<Role> roles = roleService.getRolesByUserId(user.getId());
        if(!CollectionUtils.isEmpty(roles)){
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            roles.forEach(role -> {
                simpleAuthorizationInfo.addRole(role.getName());
                PermissionService permissionService = (PermissionService) ApplicationContextUtil.getBean("permissionService");
                List<Permission> permissions = permissionService.getPermissionsByRoleId(role.getId());
                if(!CollectionUtils.isEmpty(permissions)){
                    permissions.forEach(permission -> {
                        simpleAuthorizationInfo.addStringPermission(permission.getName());
                    });
                }
            });
            return simpleAuthorizationInfo;
        }
        return null;
    }

    /** 认证代码省略 **/
}

重启项目测试

在这里插å
¥å›¾ç‰‡æè¿°

Shiro的加密和随机盐:

密码的加密策略:

实际应用中用户的密码并不是明文存储在数据库中的,而是采用一种加密算法将密码加密后存储在数据库中。Shiro中提供了一整套的加密算法,并且提供了随机盐。shiro使用指定的加密算法将用户密码和随机盐进行加密,并按照指定的散列次数将散列后的密码存储在数据库中。由于随机盐每个用户可以不同,这就极大的提高了密码的安全性。

加密方式:

import org.apache.shiro.crypto.hash.Md5Hash;

public class ShiroMD5Test {
    public static void main(String[] args){
        // MD5加密,无随机盐,无散列
        Md5Hash md5Hash01 = new Md5Hash("123456");
        System.out.println(md5Hash01.toHex());

        // MD5+随机盐加密,无散列
        Md5Hash md5Hash02 = new Md5Hash("123456","1q2w3e");
        System.out.println(md5Hash02.toHex());

        // MD5+随机盐加密+散列1024
        Md5Hash md5Hash03 = new Md5Hash("123456","1q2w3e",1024);
        System.out.println(md5Hash03.toHex());
    }
}

运行结果如下

e10adc3949ba59abbe56e057f20f883e
9eab7472e164bb8c1b823ae960467f74
41a4e25bcf1272844e38b19047dd68a0

自定义加密Realm:

CustomerMD5Realm

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;

public class CustomerMD5Realm extends AuthorizingRealm {
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 从token中获取用户名
        String principal = (String) token.getPrincipal();
        if("christy".equals(principal)){
            /**
             * 用户名
             * 加密后的密码
             * 随机盐
             * 当前realm的名称
             */
            return new SimpleAuthenticationInfo(principal,
                    "41a4e25bcf1272844e38b19047dd68a0",
                    ByteSource.Util.bytes("1q2w3e"),
                    this.getName());
        }
        return null;
    }
}

CustomerMD5AuthenticatorTest

import com.christy.shiro.realm.CustomerMD5Realm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;

public class CustomerMD5AuthenticatorTest {
    public static void main(String[] args) {
        // 创建SecurityManager
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        // 设置自定义realm
        CustomerMD5Realm realm = new CustomerMD5Realm();
        // 为realm设置凭证匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        // 设置加密算法
        credentialsMatcher.setHashAlgorithmName("md5");
        // 设置hash次数
        credentialsMatcher.setHashIterations(1024);
        realm.setCredentialsMatcher(credentialsMatcher);
        defaultSecurityManager.setRealm(realm);
        // 设置安全工具类
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        // 通过安全工具类获取subject
        Subject subject = SecurityUtils.getSubject();
        // 创建token
        UsernamePasswordToken token = new UsernamePasswordToken("christy", "123456");
        try {
            // 登录认证
            subject.login(token);
            System.out.println("认证成功");
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("用户名错误");
        } catch (IncorrectCredentialsException e) {
            e.printStackTrace();
            System.out.println("密码错误");
        }
    }
}

测试:

在这里插å
¥å›¾ç‰‡æè¿°

Springboot整合Shiro

整合一:

基本环境搭建

我们新建一个springboot工程springboot_shiro_jsp,本案例使用jsp作为前端页面展示形式,所以新建的springboot工程需要进行一下配置

pom.xml依赖

在pom文件中我们引入jsp解析的依赖

<!-- 引入jsp依赖 -->
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
</dependency>

<dependency>
    <groupId>jstl</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
</dependency>

webapp目录

main目录下我们新建一个webapp目录,目录里面新建一个jsp文件index.jsp

<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>index</title>
    </head>

    <body>
        hello world!
    </body>
</html>

application.yml配置jsp模板

spring:  
  # 设置视图模板为jsp
  mvc:
    view:
      prefix: /
      suffix: .jsp

Working directory目录

在这里插å
¥å›¾ç‰‡æè¿°

启动

以上配置完毕后,启动我们的项目,输入http://localhost:8888/index.jsp

在这里插å
¥å›¾ç‰‡æè¿°

至此,最基本的springboot集成jsp的环境已经搭建完毕了,后面我们要集成shiro

整合二:

pom.xml依赖

<!-- shiro -->
<dependency>
	<groupId>org.apache.shiro</groupId>
  	<artifactId>shiro-spring-boot-starter</artifactId>
  	<version>1.5.3</version>
</dependency>

自定义Realm

我们知道实际开发中使用shiro时都是使用自定的realm,我们先不管三七二十一,先自定义一个realm,暂时不实现认证和授权

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

/**
 * 自定义realm
 */
public class CustomerRealm extends AuthorizingRealm {
    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        return null;
    }
}

ShiroConfiguration

这个类是Shiro的核心配置类,里面继承了ShiroFilter、SecurityManager和上面的自定义的Realm

import com.christy.shiro.realm.CustomerRealm;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

/**
 * Shiro的核心配置类,用来整合shiro框架
 */
@Configuration
public class ShiroConfiguration {

    //1.创建shiroFilter  //负责拦截所有请求
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        //给filter设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);

        //配置系统受限资源
        //配置系统公共资源
        Map<String,String> map = new HashMap<String,String>();
        map.put("/index.jsp","authc");//authc 请求这个资源需要认证和授权

        //默认认证界面路径
        shiroFilterFactoryBean.setLoginUrl("/login.jsp");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);

        return shiroFilterFactoryBean;
    }

    //2.创建安全管理器
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        //给安全管理器设置
        defaultWebSecurityManager.setRealm(realm);

        return defaultWebSecurityManager;
    }

    //3.创建自定义realm
    @Bean
    public Realm getRealm(){
        CustomerRealm customerRealm = new CustomerRealm();
        return customerRealm;
    }
}

login.jsp与index.jsp

在上面的ShiroConfiguration类中,我们看到未认证的用户访问受限资源(这里指index.jsp)时会自动跳转到登录页面login.jsp,shiro默认的登录页面也是这个,我们新建一个login.jsp

<%--解决页面乱码--%>
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>index</title>
    </head>

    <body>
        <h1>用户登录</h1>
    </body>
</html>

为了与login.jsp区别开来,我们更改index.jsp的内容如下

<%--解决页面乱码--%>
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport"
              content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>login</title>
    </head>

    <body>
        <h1>系统主页</h1>

        <ul>
            <li><a href="">用户管理</a></li>
            <li><a href="">订单管理</a></li>
        </ul>
    </body>
</html>

启动

以上工作准备完毕后,我们启动项目再次访问index.jsp,发现页面跳转到登录页面。这就说明我们的项目中成功的引入了Shiro

整合三:

Shiro快速开始

准备工作

GitHub资源

  • 创建一个普通maven项目springboot-08-shiro,然后删除src目录,这样的话就可以在这个项目里新建很多model.
  • springboot-08-shiro里新建model hello_shiro
  • 找到文件

在这里插å
¥å›¾ç‰‡æè¿°

pom.xml中复制

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

        <!-- configure logging -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <scope>runtime</scope>
        </dependency>
    </dependencies>

更改细节

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

        <!-- configure logging -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>1.7.21</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.21</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
            <scope>runtime</scope>
        </dependency>
    </dependencies>

复制

在这里插å
¥å›¾ç‰‡æè¿°

log4j.properties

log4j.rootLogger=INFO, stdout

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n

# General Apache libraries
log4j.logger.org.apache=WARN

# Spring
log4j.logger.org.springframework=WARN

# Default Shiro logging
log4j.logger.org.apache.shiro=INFO

# Disable verbose logging
log4j.logger.org.apache.shiro.util.ThreadContext=WARN
log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN

shiro.ini

[users]
# user 'root' with password 'secret' and the 'admin' role
root = secret, admin
# user 'guest' with the password 'guest' and the 'guest' role
guest = guest, guest
# user 'presidentskroob' with password '12345' ("That's the same combination on
# my luggage!!!" ;)), and role 'president'
presidentskroob = 12345, president
# user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz'
darkhelmet = ludicrousspeed, darklord, schwartz
# user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz'
lonestarr = vespa, goodguy, schwartz

# -----------------------------------------------------------------------------
# Roles with assigned permissions
# 
# Each line conforms to the format defined in the
# org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc
# -----------------------------------------------------------------------------
[roles]
# 'admin' role has all permissions, indicated by the wildcard '*'
admin = *
# The 'schwartz' role can do anything (*) with any lightsaber:
schwartz = lightsaber:*
# The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with
# license plate 'eagle5' (instance specific id)
goodguy = winnebago:drive:eagle5

然后

在这里插å
¥å›¾ç‰‡æè¿°

Quickstart.java

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.ini.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.lang.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * Simple Quickstart application showing how to use Shiro's API.
 *
 * @since 0.9 RC2
 */
public class Quickstart {

    private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);


    public static void main(String[] args) {

        // The easiest way to create a Shiro SecurityManager with configured
        // realms, users, roles and permissions is to use the simple INI config.
        // We'll do that by using a factory that can ingest a .ini file and
        // return a SecurityManager instance:

        // Use the shiro.ini file at the root of the classpath
        // (file: and url: prefixes load from files and urls respectively):
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityManager securityManager = factory.getInstance();

        // for this simple example quickstart, make the SecurityManager
        // accessible as a JVM singleton.  Most applications wouldn't do this
        // and instead rely on their container configuration or web.xml for
        // webapps.  That is outside the scope of this simple quickstart, so
        // we'll just do the bare minimum so you can continue to get a feel
        // for things.
        SecurityUtils.setSecurityManager(securityManager);

        // Now that a simple Shiro environment is set up, let's see what you can do:

        // get the currently executing user:
        Subject currentUser = SecurityUtils.getSubject();

        // Do some stuff with a Session (no need for a web or EJB container!!!)
        Session session = currentUser.getSession();
        session.setAttribute("someKey", "aValue");
        String value = (String) session.getAttribute("someKey");
        if (value.equals("aValue")) {
            log.info("Retrieved the correct value! [" + value + "]");
        }

        // let's login the current user so we can check against roles and permissions:
        if (!currentUser.isAuthenticated()) {
            UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
            token.setRememberMe(true);
            try {
                currentUser.login(token);
            } catch (UnknownAccountException uae) {
                log.info("There is no user with username of " + token.getPrincipal());
            } catch (IncorrectCredentialsException ice) {
                log.info("Password for account " + token.getPrincipal() + " was incorrect!");
            } catch (LockedAccountException lae) {
                log.info("The account for username " + token.getPrincipal() + " is locked.  " +
                        "Please contact your administrator to unlock it.");
            }
            // ... catch more exceptions here (maybe custom ones specific to your application?
            catch (AuthenticationException ae) {
                //unexpected condition?  error?
            }
        }

        //say who they are:
        //print their identifying principal (in this case, a username):
        log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");

        //test a role:
        if (currentUser.hasRole("schwartz")) {
            log.info("May the Schwartz be with you!");
        } else {
            log.info("Hello, mere mortal.");
        }

        //test a typed permission (not instance-level)
        if (currentUser.isPermitted("lightsaber:wield")) {
            log.info("You may use a lightsaber ring.  Use it wisely.");
        } else {
            log.info("Sorry, lightsaber rings are for schwartz masters only.");
        }

        //a (very powerful) Instance Level permission:
        if (currentUser.isPermitted("winnebago:drive:eagle5")) {
            log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'.  " +
                    "Here are the keys - have fun!");
        } else {
            log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
        }

        //all done - log out!
        currentUser.logout();

        System.exit(0);
    }
}

启动

2020-11-04 09:13:59,275 INFO [org.apache.shiro.session.mgt.AbstractValidatingSessionManager] - Enabling session validation scheduler... 
2020-11-04 09:14:00,629 INFO [Quickstart] - Retrieved the correct value! [aValue] 
2020-11-04 09:14:00,630 INFO [Quickstart] - User [lonestarr] logged in successfully. 
2020-11-04 09:14:00,631 INFO [Quickstart] - May the Schwartz be with you! 
2020-11-04 09:14:00,631 INFO [Quickstart] - You may use a lightsaber ring.  Use it wisely. 
2020-11-04 09:14:00,632 INFO [Quickstart] - You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'.  Here are the keys - have fun! 
  • 新建SpringBoot项目,勾选webthymeleaf
  • 保持项目清洁,删除

在这里插å
¥å›¾ç‰‡æè¿°

  • templates下新建index.html
<!DOCTYPE html>
<html lang="en"
      xmlns:th="https://www.thymeleaf.org"
      xmlns:shiro="https://www.thymeleaf.org/thymeleaf-extras-shiro">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<h1>首页</h1>

<p th:text="${msg}"></p>

</body>
</html>
  • controller包下新建MyController
package com.huang.controller;

import org.apache.catalina.security.SecurityUtil;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpSession;

@Controller
public class MyController {

    @RequestMapping({"/","/index"})
    public String toIndex(Model model){
        model.addAttribute("msg","hello,shiro");
        return "index";
    }
}
  • 测试正常

在这里插å
¥å›¾ç‰‡æè¿°

让我们继续

Subject用户
SecurityManager管理所有用户
Realm连接数据

GitHub资源

在这里插å
¥å›¾ç‰‡æè¿°

pom.xml复制

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-web-starter</artifactId>
        </dependency>

发现并不好使更换为

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

shiro整合mybatis

导入jar包

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.19</version>
        </dependency>

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

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.12</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>

编写application.yml

spring:
  datasource:
    username: root
    password: jia5211314
    url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

    #Spring Boot 默认是不注入这些属性值的,需要自己绑定
    #druid 数据源专有配置
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true

    #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
    #如果允许时报错  java.lang.ClassNotFoundException: org.apache.log4j.Priority
    #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
    filters: stat,wall,log4j
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
mybatis:
  type-aliases-package: com.huang.pojo
  mapper-locations: classpath:mybatis/mapper/*.xml

编写实体类

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.10</version>
            <scope>provided</scope>
        </dependency>

User

package com.huang.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private int id;
    private String name;
    private String pwd;
    private String perms;


}

mapper包编写UserMapper

package com.huang.mapper;

import com.huang.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;

import java.util.List;

@Mapper//这个注解表示了这是一个mybatis的mapper类
@Repository
public interface UserMapper {

    List<User> queryUserList();

    User queryUserById(int id);

    int addUser(User user);

    int updateUser(User user);

    int deleteUser(int id);
}

resource包下新建mybatis包下健mapper
UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.huang.mapper.UserMapper">

    <select id="queryUserList" resultType="User">
       select * from user;
    </select>

    <select id="queryUserByName" resultType="User" parameterType="String">
        select * from user where name=#{name}
    </select>

</mapper>

service

UserService接口

package com.huang.service;

import com.huang.pojo.User;

public interface UserService {
    User queryUserByName(String name);
}

UserServiceImpl

package com.huang.service;

import com.huang.mapper.UserMapper;
import com.huang.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService{
    @Autowired
    UserMapper userMapper;

    @Override
    public User queryUserByName(String name) {
        return userMapper.queryUserByName(name);
    }
}

ShiroSpringbootApplicationTests中进行测试

package com.huang;

import com.huang.service.UserServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class ShiroSpringbootApplicationTests {
    @Autowired
    UserServiceImpl userService;

    @Test
    void contextLoads() {
        System.out.println(userService.queryUserByName("张三"));
    }

}
  • 测试成功

Shiro整合Thymeleaf

导入jar

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

代码整理

  • 结构目录

在这里插å
¥å›¾ç‰‡æè¿°

config

ShiroConfig

package com.huang.config;

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {

    //ShiroFilterBean
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean bean=new ShiroFilterFactoryBean();
        //设置安全管理器
        bean.setSecurityManager(defaultWebSecurityManager);

        //添加shiro的内置过滤器
        /*
            anon:无需认证就能访问
            authc:必须认证才能访问
            user:必须拥有记住我功能才能访问
            perms:拥有某个资源的权限才能访问
            role:拥有某个角色权限才能访问
         */
        //拦截
        Map<String,String> filterMap =new LinkedHashMap<>();

        //授权
        filterMap.put("/user/add","perms[user:add]");
        filterMap.put("/user/update","perms[user:update]");

        //filterMap.put("/user/add","authc");
        //filterMap.put("/user/update","authc");

        //设置登陆的请求
        bean.setLoginUrl("/toLogin");

        //设置未授权的请求
        bean.setUnauthorizedUrl("/noauth");

        bean.setFilterChainDefinitionMap(filterMap);

        return bean;

    }

    //DefaultWebSecurityManager
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
        DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();

        //关联UserRealm
        securityManager.setRealm(userRealm);

        return securityManager;
    }


    //创建realm对象
    @Bean
    public UserRealm userRealm(){
        return new UserRealm();
    }

    //整合shiroDialect:用来整合shiro thymeleaf
    @Bean
    public ShiroDialect getShiroDialect(){
        return new ShiroDialect();
    }
}

UserRealm

package com.huang.config;

import com.huang.pojo.User;
import com.huang.service.UserService;
import org.apache.catalina.security.SecurityUtil;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;

public class UserRealm extends AuthorizingRealm {
    @Autowired
    UserService userService;

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("执行了授权");

        SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();

        //info.addStringPermission("user:add");
        //拿到当前用户登陆对象
        Subject subject= SecurityUtils.getSubject();
        User currentUser= (User) subject.getPrincipal();//拿到User对象
        info.addStringPermission(currentUser.getPerms());//设置当前用户对象

        return info;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("执行了认证");

        //用户名,密码,数据库中获取

        UsernamePasswordToken userToken=(UsernamePasswordToken) authenticationToken;

        User user=userService.queryUserByName(userToken.getUsername());//获取用户名

        String name=user.getName();
        String password=user.getPwd();
        if(user==null){//说明查无此人
            return null;
        }

        //密码认证,shiro做
        return new SimpleAuthenticationInfo(user,password,"");//放入User对象

    }
}

controller

MyController

package com.huang.controller;

import org.apache.catalina.security.SecurityUtil;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpSession;

@Controller
public class MyController {

    @RequestMapping({"/","/index"})
    public String toIndex(Model model){
        model.addAttribute("msg","hello,shiro");
        return "index";
    }

    @RequestMapping("/user/add")
    public String add(){
        return "user/add";
    }

    @RequestMapping("/user/update")
    public String update(){
        return "user/update";
    }

    @RequestMapping("/toLogin")
    public String toLogin(){
        return "login";
    }

    @RequestMapping("/login")
    public String login(String username, String password, Model model, HttpSession session){

        //获取当前用户
        Subject subject= SecurityUtils.getSubject();
        //封装用户的登陆数据
        UsernamePasswordToken token=new UsernamePasswordToken(username,password);

        try{
            subject.login(token);//执行登陆的方法
            session.setAttribute("loginUser",username);//设置session
            return "index";
        }catch (UnknownAccountException e){//用户名不存在
            model.addAttribute("msg","用户名错误");
            return "login";
        }catch (IncorrectCredentialsException e){//密码不存在
            model.addAttribute("msg","密码错误");
            return "login";
        }

    }

    @RequestMapping("/noauth")
    @ResponseBody
    public String unauthorized(){
        return "未经授权禁止访问";

    }

}

mapper

UserMapper

package com.huang.mapper;

import com.huang.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;

import java.util.List;

@Mapper
@Repository
public interface UserMapper {

    User queryUserByName(String name);

    List<User> queryUserList();

    User queryUserById(int id);

    int addUser(User user);

    int updateUser(User user);

    int deleteUser(int id);
}

pojo

User

package com.huang.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private int id;
    private String name;
    private String pwd;
    private String perms;
}

service

UserService

package com.huang.service;

import com.huang.pojo.User;

public interface UserService {
    User queryUserByName(String name);
}

UserServiceImpl

package com.huang.service;

import com.huang.mapper.UserMapper;
import com.huang.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService{
    @Autowired
    UserMapper userMapper;

    @Override
    public User queryUserByName(String name) {
        return userMapper.queryUserByName(name);
    }
}

resource

UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.huang.mapper.UserMapper">

    <select id="queryUserList" resultType="User">
       select * from user;
    </select>

    <select id="queryUserByName" resultType="User" parameterType="String">
        select * from user where name=#{name}
    </select>

</mapper>

add.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>添加一个用户</h1>
</body>
</html>


-----------------------------------------------

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>修改一个用户</h1>
</body>
</html>

-----------------------------------------------
<!DOCTYPE html>
<html lang="en"
      xmlns:th="https://www.thymeleaf.org"
      xmlns:shiro="https://www.thymeleaf.org/thymeleaf-extras-shiro">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<h1>首页</h1>

<p th:text="${msg}"></p>

<hr>
<div th:if="${session.loginUser==null}">
    <a th:href="@{/toLogin}">登陆</a>
</div>

<div shiro:hasPermission="user:add">
    <a th:href="@{/user/add}">add</a>
</div>

<div shiro:hasPermission="user:update">
    <a th:href="@{/user/update}">update</a>
</div>

</body>
</html>

-----------------------------------------------

<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>登陆</h1>
<hr>
<p style="color: red" th:text="${msg}"></p>
<form th:action="@{/login}">
    <p>用户名:<input type="text" name="username"></p>
    <p>密码:<input type="password" name="password"></p>
    <p><input type="submit"></p>
</form>
</body>
</html>

application.yml

spring:
  datasource:
    username: root
    password: jia5211314
    url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

    #Spring Boot 默认是不注入这些属性值的,需要自己绑定
    #druid 数据源专有配置
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true

    #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
    #如果允许时报错  java.lang.ClassNotFoundException: org.apache.log4j.Priority
    #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
    filters: stat,wall,log4j
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
mybatis:
  type-aliases-package: com.huang.pojo
  mapper-locations: classpath:mybatis/mapper/*.xml

pom.xml

    <dependencies>

        <!-- https://mvnrepository.com/artifact/com.github.theborakompanioni/thymeleaf-extras-shiro -->
        <dependency>
            <groupId>com.github.theborakompanioni</groupId>
            <artifactId>thymeleaf-extras-shiro</artifactId>
            <version>2.0.0</version>
        </dependency>


        <!--
        Subject 用户
        SecurityManager 管理所有用户
        Realm 连接数据
        -->

        <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.19</version>
        </dependency>

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

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

        <!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>

        <!--lombok-->
        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.10</version>
            <scope>provided</scope>
        </dependency>


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

        <!--thymeleaf模板-->
        <dependency>
            <groupId>org.thymeleaf</groupId>
            <artifactId>thymeleaf-spring5</artifactId>
        </dependency>

        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-java8time</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

小结

此部分比Spring Security较难,并未能很好的理解所以粘贴了大量代码,回头会再次理解,对笔记进行补充.

整合四:(基于url)

项目运行图:

img

权限管理原理知识:

什么是权限管理
只要有用户参与的系统一般都要有权限管理,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。

权限管理包括用户认证和授权两部分。

用户认证

概念
用户认证,用户去访问系统,系统要验证用户身份的合法性。最常用的用户身份验证的方法:1、用户名密码方式、2、指纹打卡机、3、基于证书验证方法。。系统验证用户身份合法,用户方可访问系统的资源。

用户认证流程
img

关键对象
subject:主体,理解为用户,可能是程序,都要去访问系统的资源,系统需要对subject进行身份认证。

principal:身份信息,通常是唯一的,一个主体还有多个身份信息,但是都有一个主身份信息(primary principal)

credential:凭证信息,可以是密码 、证书、指纹。

总结:主体在进行身份认证时需要提供身份信息和凭证信息。

用户授权

概念

用户授权,简单理解为访问控制,在用户认证通过后,系统对用户访问资源进行控制,用户具有资源的访问权限方可访问。

授权流程

img

关键对象

授权的过程理解为:who对what(which)进行how操作。

who:主体即subject,subject在认证通过后系统进行访问控制。

what(which):资源(Resource),subject必须具备资源的访问权限才可访问该 资源。资源比如:系统用户列表页面、商品修改菜单、商品id为001的商品信息。

资源分为资源类型和资源实例:

系统的用户信息就是资源类型,相当于java类。

系统中id为001的用户就是资源实例,相当于new的java对象。

how:权限/许可(permission) ,针对资源的权限或许可,subject具有permission访问资源,如何访问/操作需要定义permission,权限比如:用户添加、用户修改、商品删除。

权限模型

主体(账号、密码)

资源(资源名称、访问地址)

权限(权限名称、资源id)

角色(角色名称)

角色和权限关系(角色id、权限id)

主体和角色关系(主体id、角色id)

如下图:

img

通常企业开发中将资源和权限表合并为一张权限表,如下:

资源(资源名称、访问地址)

权限(权限名称、资源id)

合并为:

权限(权限名称、资源名称、资源访问地址)

img

上图常被称为权限管理的通用模型,不过企业在开发中根据系统自身的特点还会对上图进行修改,但是用户、角色、权限、用户角色关系、角色权限关系是需要去理解的。

分配权限

用户需要分配相应的权限才可访问相应的资源。权限是对于资源的操作许可。

通常给用户分配资源权限需要将权限信息持久化,比如存储在关系数据库中。

把用户信息、权限管理、用户分配的权限信息写到数据库(权限数据模型)

权限控制(授权核心)

基于角色的访问控制
RBAC(role based access control),基于角色的访问控制。

比如:

系统角色包括 :部门经理、总经理。。(角色针对用户来划分)

系统代码中实现:

//如果该user是部门经理则可以访问if中的代码

if(user.hasRole('部门经理')){
         //系统资源内容
         //用户报表查看
}

问题:

角色针对人划分的,人作为用户在系统中属于活动内容,如果该 角色可以访问的资源出现变更,需要修改你的代码了,比如:需要变更为部门经理和总经理都可以进行用户报表查看,代码改为:

if(user.hasRole('部门经理') || user.hasRole('总经理')  ){
         //系统资源内容

         //用户报表查看

}

基于角色的访问控制是不利于系统维护(可扩展性不强)。

基于资源的访问控制

RBAC(Resource based access control),基于资源的访问控制。

资源在系统中是不变的,比如资源有:类中的方法,页面中的按钮。

对资源的访问需要具有permission权限,代码可以写为:

if(user.hasPermission ('用户报表查看(权限标识符)')){
         //系统资源内容

         //用户报表查看

}

上边的方法就可以解决用户角色变更不用修改上边权限控制的代码。

如果需要变更权限只需要在分配权限模块去操作,给部门经理或总经理增或删除权限。

建议使用基于资源的访问控制实现权限管理。

权限管理解决方案:

什么是粗粒度和细粒度权限

粗粒度权限管理,对资源类型的权限管理。资源类型比如:菜单、url连接、用户添加页面、用户信息、类方法、页面中按钮。。

粗粒度权限管理比如:超级管理员可以访问户添加页面、用户信息等全部页面。

部门管理员可以访问用户信息页面包括 页面中所有按钮。

细粒度权限管理,对资源实例的权限管理。资源实例就资源类型的具体化,比如:用户id为001的修改连接,1110班的用户信息、行政部的员工。

细粒度权限管理就是数据级别的权限管理。

细粒度权限管理比如:部门经理只可以访问本部门的员工信息,用户只可以看到自己的菜单,大区经理只能查看本辖区的销售订单。。

粗粒度和细粒度例子:

系统有一个用户列表查询页面,对用户列表查询分权限,如果粗颗粒管理,张三和李四都有用户列表查询的权限,张三和李四都可以访问用户列表查询。

进一步进行细颗粒管理,张三(行政部)和李四(开发部)只可以查询自己本部门的用户信息。张三只能查看行政部 的用户信息,李四只能查看开发部门的用户信息。细粒度权限管理就是数据级别的权限管理。

如何实现粗粒度和细粒度权限管理

如何实现粗粒度权限管理?

粗粒度权限管理比较容易将权限管理的代码抽取出来在系统架构级别统一处理。比如:通过springmvc的拦截器实现授权。

如何实现细粒度权限管理?

对细粒度权限管理在数据级别是没有共性可言,针对细粒度权限管理就是系统业务逻辑的一部分,如果在业务层去处理相对比较简单,如果将细粒度权限管理统一在系统架构级别去抽取,比较困难,即使抽取的功能可能也存在扩展不强。

建议细粒度权限管理在业务层去控制。

比如:部门经理只查询本部门员工信息,在service接口提供一个部门id的参数,controller中根据当前用户的信息得到该 用户属于哪个部门,调用service时将部门id传入service,实现该用户只查询本部门的员工。

基于url拦截的方式实现

基于url拦截的方式实现在实际开发中比较常用的一种方式。

对于web系统,通过filter过虑器实现url拦截,也可以springmvc的拦截器实现基于url的拦截。

使用权限管理框架实现

对于粗粒度权限管理,建议使用优秀权限管理框架来实现,节省开发成功,提高开发效率。

shiro就是一个优秀权限管理框架。

基于url的权限管理:

基于url权限管理流程

img

公开地址:比如登陆页面,注册页面等

公共地址:比如登陆成功的首页,欢迎页面等

  1. 搭建环境
  2. 数据库

mysql5.1数据库中创建表:用户表、角色表、权限表(实质上是权限和资源的结合 )、用户角色表、角色权限表。

img

完成权限管理的数据模型创建。

img

开发环境

jdk1.7.0_72

eclipse 3.7 indigo

技术架构:springmvc+mybatis+jquery easyui

系统工程架构

springmvc+mybatis+jquery easyui

系统登陆

系统 登陆相当 于用户身份认证,用户成功,要在session中记录用户的身份信息.

操作流程:

用户进行登陆页面

输入用户名和密码进行登陆

进行用户名和密码校验

如果校验通过,在session记录用户身份信息

用户的身份信息

创建专门类用于记录用户身份信息。

img

mapper

mapper接口: 根据用户账号查询用户(sys_user)信息(使用逆向工程生成的mapper)

使用逆向工程生成以下表的基础代码:

img

service(进行用户名和密码校验)

接口功能:根据用户的身份和密码 进行认证,如果认证通过,返回用户身份信息

认证过程:

​ 根据用户身份(账号)查询数据库,如果查询不到用户不存在

​ 对输入的密码 和数据库密码 进行比对,如果一致,认证通过

img

controller(记录session)

img

用户认证拦截器:

anonymousURL.properties

配置可以匿名访问的url

img

编写认证拦截器

//用于用户认证校验、用户权限校验
   @Override
   public boolean preHandle(HttpServletRequest request,
         HttpServletResponse response, Object handler) throws Exception {
      //得到请求的url
      String url = request.getRequestURI();
      //判断是否是公开 地址
      //实际开发中需要公开 地址配置在配置文件中
      //从配置中取逆名访问url
      List<String> open_urls = ResourcesUtil.gekeyList("anonymousURL");
      //遍历公开 地址,如果是公开 地址则放行
      for(String open_url:open_urls){
         if(url.indexOf(open_url)>=0){
            //如果是公开 地址则放行
            return true;
         }
      }
      //判断用户身份在session中是否存在
      HttpSession session = request.getSession();
      ActiveUser activeUser = (ActiveUser) session.getAttribute("activeUser");
      //如果用户身份在session中存在放行
      if(activeUser!=null){
         return true;
      }
      //执行到这里拦截,跳转到登陆页面,用户进行身份认证
      request.getRequestDispatcher("/WEB-INF/jsp/login.jsp").forward(request, response);
      //如果返回false表示拦截不继续执行handler,如果返回true表示放行
      return false;
   }

配置拦截器

在springmvc.xml中配置拦截器

img

授权:

commonURL.properties

在此配置文件配置公用访问地址,公用访问地址只要通过用户认证,不需要对公用访问地址分配权限即可访问。

获取用户权限范围的菜单

思路:

在用户认证时,认证通过,根据用户id从数据库获取用户权限范围的菜单,将菜单的集合存储在session中。

img

mapper接口:根据用户id查询用户权限的菜单

img

img

service接口:根据用户id查询用户权限的菜单

img

获取用户权限范围的url

思路:

在用户认证时,认证通过,根据用户id从数据库获取用户权限范围的url,将url的集合存储在session中。

img

mapper接口:根据用户id查询用户权限的url

img

img

service接口:根据用户id查询用户权限的url

img

用户认证通过取出菜单和url放入session

修改service认证代码:

img

菜单动态显示

修改first.jsp,动态从session中取出菜单显示:

img

授权拦截器

//在执行handler之前来执行的
   //用于用户认证校验、用户权限校验
   @Override
   public boolean preHandle(HttpServletRequest request,
         HttpServletResponse response, Object handler) throws Exception {
      //得到请求的url
      String url = request.getRequestURI();
      //判断是否是公开 地址
      //实际开发中需要公开 地址配置在配置文件中
      //从配置中取逆名访问url
      List<String> open_urls = ResourcesUtil.gekeyList("anonymousURL");
      //遍历公开 地址,如果是公开 地址则放行
      for(String open_url:open_urls){
         if(url.indexOf(open_url)>=0){
            //如果是公开 地址则放行
            return true;
         }
      }
      //从配置文件中获取公共访问地址
      List<String> common_urls = ResourcesUtil.gekeyList("commonURL");
      //遍历公用 地址,如果是公用 地址则放行
      for(String common_url:common_urls){
         if(url.indexOf(common_url)>=0){
            //如果是公开 地址则放行
            return true;
         }
      }
      //获取session
      HttpSession session = request.getSession();
      ActiveUser activeUser = (ActiveUser) session.getAttribute("activeUser");
      //从session中取权限范围的url
      List<SysPermission> permissions = activeUser.getPermissions();
      for(SysPermission sysPermission:permissions){
         //权限的url
         String permission_url = sysPermission.getUrl();
         if(url.indexOf(permission_url)>=0){
            //如果是权限的url 地址则放行
            return true;
         }
      }
      //执行到这里拦截,跳转到无权访问的提示页面
      request.getRequestDispatcher("/WEB-INF/jsp/refuse.jsp").forward(request, response);
      //如果返回false表示拦截不继续执行handler,如果返回true表示放行
      return false;
   }

配置授权拦截器

注意:将授权拦截器配置在用户认证拦截的下边。

img

小结:

使用基于url拦截的权限管理方式,实现起来比较简单,不依赖框架,使用web提供filter就可以实现。

问题:

需要将所有的url全部配置起来,有些繁琐,不易维护,url(资源)和权限表示方式不规范。

整合五:(带配置)

认证过程中会发生的异常

Subject subject = SecurityUtils.getSubject();
if(!subject.isAuthenticated()){
  UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(userName,password);
  usernamePasswordToken.setRememberMe(true);
  try{
     subject.login(usernamePasswordToken);
  }catch (UnknownAccountException e){
     System.out.print("账户不存在:"+e.getMessage());
  }catch (LockedAccountException e){
     System.out.print("账户被锁定:"+e.getMessage());
  }catch (IncorrectCredentialsException e){
     System.out.print("密码不匹配:"+e.getMessage());
  }
}

异常的体系

img

3.1 添加依赖

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

3.2 相关配置:

3.2.1 配置Realm及多Realm的认证策略

/***
* 配置Realm
* */
@Bean
Realm userRealm() {
    TestRealm userRealm = new TestRealm();
    return userRealm;
}
/***
 * 测试realm
 * @return
 */
@Bean
Realm testRealm() {
    return new TestRealm();
}
 
/**
 * 多Realm认证策略
 * @return
 */
@Bean
Authenticator authenticator() {
    ModularRealmAuthenticator modularRealmAuthenticator = new ModularRealmAuthenticator();
    //使用一个通过验证即可通过验证的方式
    modularRealmAuthenticator.setAuthenticationStrategy(atLeastOneSuccessfulStrategy());
    return modularRealmAuthenticator;
}
/**
 * 多使用所有reaml均通过才算认证通过作为策略
 * @return
 */
@Bean
AuthenticationStrategy allSuccessfulStrategy() {
    AllSuccessfulStrategy allSuccessfulStrategy = new AllSuccessfulStrategy();
    return allSuccessfulStrategy;
}
/**
 * 多使用所有reaml一个通过就算认证通过作为策略
 * @return
 */
@Bean
AuthenticationStrategy atLeastOneSuccessfulStrategy() {
    AtLeastOneSuccessfulStrategy atLeastOneSuccessfulStrategy = new AtLeastOneSuccessfulStrategy();
    return atLeastOneSuccessfulStrategy;
}

3.2.2 配置Cache

需要加入ehcache的jar包及配置文件

@Bean
public EhCacheManager ehCacheManager() {
    EhCacheManager em = new EhCacheManager();
    em.setCacheManager(cacheManager());
    em.setCacheManagerConfigFile("classpath: ehcach.xml");
    return em;
}
 
@Bean("cacheManager2")
CacheManager cacheManager(){
    return CacheManager.create();
}

3.2.3 配置securityManager

/***
 * 配置securityManager
 * @return
 */
@Bean
public SecurityManager securityManager() {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    //设置单个realm.
    securityManager.setRealm(userRealm());
    //设置多个realm 授权验证时 只要一个通过就可以
    List<Realm> realms = new ArrayList<>();
    realms.add(userRealm());
    realms.add(userRealm());
    securityManager.setRealms(realms);
    // 自定义缓存实现 使用redis
//        if (Constant.CACHE_TYPE_REDIS.equals(cacheType)) {
//            securityManager.setCacheManager(rediscacheManager());
//        } else {
        securityManager.setCacheManager(ehCacheManager());
    //}
    //securityManager.setSessionManager(sessionManager());
    //多Reaml的认证策略
    securityManager.setAuthenticator(authenticator());
    return securityManager;
}

3.2.4 配置shiroFilterFactoryBean 其实就是Filter的规则

/***
 *  配置shiroFilterFactoryBean
 *  配置基本的过滤规则
 * @param securityManager
 * @return
 */
@Bean
ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    //配置管理器
    shiroFilterFactoryBean.setSecurityManager(securityManager);
    //配置登陆\登陆成功页面\登陆失败页面
    shiroFilterFactoryBean.setLoginUrl("/login.html");
    shiroFilterFactoryBean.setSuccessUrl("/index.html");
    shiroFilterFactoryBean.setUnauthorizedUrl("/403.html");
    //配置基本的策略规则,那些可以直接访问  那些需要权限  按照顺序,先配置的起作用
    //支持Ant风格 
    LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
    filterChainDefinitionMap.put("/login.html","anon");
    filterChainDefinitionMap.put("**/login/**","anon");
    filterChainDefinitionMap.put("/css/**", "anon");
    filterChainDefinitionMap.put("/js/**", "anon");
    filterChainDefinitionMap.put("/fonts/**", "anon");
    filterChainDefinitionMap.put("/img/**", "anon");
    filterChainDefinitionMap.put("/docs/**", "anon");
    filterChainDefinitionMap.put("/druid/**", "anon");
    filterChainDefinitionMap.put("/upload/**", "anon");
    filterChainDefinitionMap.put("/files/**", "anon");
    //配置注销的URL
    filterChainDefinitionMap.put("/logout", "logout");
    //
    filterChainDefinitionMap.put("/**", "authc");
    shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    return shiroFilterFactoryBean;
}

4 . 以上为配置: 接下来实际进行验证

4.1 身份认证代码流程:

• 1、首先调用 Subject.login(token) 进行登录,其会自动委托给 SecurityManager

• 2、SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;

• 3、Authenticator 才是真正的身份验证者,Shiro API 中核心的身份 认证入口点,此处可以自定义插入自己的实现;

• 4、Authenticator 可能会委托给相应的 AuthenticationStrategy 进 行多 Realm 身份验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm 身份验证;

• 5、Authenticator 会把相应的 token 传入 Realm,从 Realm 获取 身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处 可以配置多个Realm,将按照相应的顺序及策略进行访问。

4.2 实现流程思路:

img

4.3 实现代码示例

img

5. Realm的认证和授权

5.1 具体代码, 来个例子,看相关注释: Cope 可用

 
public class TestRealm extends AuthorizingRealm {
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //关于JWT的考虑,密码登陆还是要的,但是要多个Reamle同时用,
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken)authenticationToken;
        String username = usernamePasswordToken.getUsername();
        System.out.print("根据username查询yoghurt");
        if("none".equals(username)){
           throw new UnknownAccountException("用户不存在");
        }
        //认证的实体信息, 可以放对象,以后可以随时取出来用,  JSP标签也可以直接去这个对象
        Object principal = username;
        //数据库中获取的密码
        Object credentials = "1234567";
        //封装一个带数据的对象,Shiro会拿这个对象和传进来的Token的密码进行对比,验证是否登录成功
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal, credentials, getName());
        return simpleAuthenticationInfo;
    }
    //这个方法是用来授权的
    //查询登陆人是否有权限时就查询这个  如果设置了缓存,只会查一次
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //这个里面取的就是登陆认证时SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, password, getName());的第一个参数
        Object user = principalCollection.getPrimaryPrincipal();
        //将其转为自己的用户,在从数据库获取到这个用户的角色  权限
        //MenuService menuService = ApplicationContextRegister.getBean(MenuService.class);
        //这里只查询了权限
        //Set<String> perms = menuService.listPerms(userId);
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //放进去即可
        //info.setStringPermissions(perms);
        return info;
    }
}

5.2 多Realm认证策略: AuthenticationStrategy

• AuthenticationStrategy 接口的默认实现:

• FirstSuccessfulStrategy:只要有一个 Realm 验证成功即可,只返回第 一个 Realm 身份验证成功的认证信息,其他的忽略;

• AtLeastOneSuccessfulStrategy:只要有一个Realm验证成功即可,和 FirstSuccessfulStrategy 不同,将返回所有Realm身份验证成功的认证信 息;

• AllSuccessfulStrategy:所有Realm验证成功才算成功,且返回所有 Realm身份验证成功的认证信息,如果有一个失败就失败了。 • ModularRealmAuthenticator 默认是 AtLeastOneSuccessfulStrategy 策略

上面的代码有具体的实现

6. 代码使用权限的方式:

img

6.2 权限的规则规定:

img

img

6.3 JSP 的标签:

img

img

img

img

来一个比较常用的:

img

img

6.4 JAVA的相关注解

开启注解:

/***
 * 
 * 开启生命周期, 可以自定义的来调用配置SPring-ioc 容器中shiro bean 的生命周期方法
 * 尚未研究怎么自定义
 * @return
 */
@Bean
public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
    return new LifecycleBeanPostProcessor();
}
 
/**
 * 开启shiro aop注解支持.
 * 使用代理方式;所以需要开启代码支持;
 * 要开启代码支持必须开启上面的生命周期
 * @param securityManager
 * @return
 */
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
    AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
    authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
    return authorizationAttributeSourceAdvisor;
}

img

可以用在Controller或者Service层上,但是如果Serivce上加了事务的注解,就不能再加这个了,原因是不是事务产生代理了,不能是代理的代理

7. 盐值加密:

这里举个例子: 使用Shiro的验证还不如自己进行验证, 上一个使用用户名最为盐值, 对字符串使用1024次加密的用例;

img

8. 缓存

8.1授权缓存:

  1. Shiro 内部相应的组件(DefaultSecurityManager)会自 动检测相应的对象(如Realm)是否实现了 CacheManagerAware 并自动注入相应的 CacheManager。
  2. Shiro 提供了 CachingRealm,其实现了 CacheManagerAware 接口,提供了缓存的一些基础实现;
  3. AuthenticatingRealm 及 AuthorizingRealm 也分别提 供了对AuthenticationInfo 和 AuthorizationInfo 信息的缓 存。所以默认是启用了缓存的

8.2 Session缓存:

如 SecurityManager 实现了 SessionSecurityManager, 其会判断 SessionManager 是否实现了 CacheManagerAware 接口,如果实现了会把 CacheManager 设置给它。

• SessionManager 也会判断相应的 SessionDAO(如继承 自CachingSessionDAO)是否实现了 CacheManagerAware,如果实现了会把 CacheManager 设置给它

• 设置了缓存的 SessionManager,查询时会先查缓存,如 果找不到才查数据库。

9. Session的会话管理:

Shiro 提供了完整的企业级会话管理功能,不依赖于底层容 器(如web容器tomcat),不管 JavaSE 还是 JavaEE 环境 都可以使用,提供了会话管理、会话事件监听、会话存储/ 持久化、容器无关的集群、失效/过期支持、对Web 的透明 支持、SSO 单点登录的支持等特性。

和HttpSession互通, 但你可以在任何层级获取到Session

9.1 会话相关的 API

• Subject.getSession():即可获取会话;其等价于 Subject.getSession(true),即如果当前没有创建 Session 对象会创建 一个;Subject.getSession(false),如果当前没有创建 Session 则返回 null

• session.getId():获取当前会话的唯一标识

• session.getHost():获取当前Subject的主机地址

• session.getTimeout() & session.setTimeout(毫秒):获取/设置当 前Session的过期时间

• session.getStartTimestamp() & session.getLastAccessTime(): 获取会话的启动时间及最后访问时间;如果是 JavaSE 应用需要自己定 期调用 session.touch() 去更新最后访问时间;如果是 Web 应用,每 次进入 ShiroFilter 都会自动调用 session.touch() 来更新最后访问时间。
• session.touch() & session.stop():更新会话最后访问时 间及销毁会话;当Subject.logout()时会自动调用 stop 方法 来销毁会话。如果在web中,调用 HttpSession. invalidate() 也会自动调用Shiro Session.stop 方法进行销毁Shiro 的会 话

• session.setAttribute(key, val) & session.getAttribute(key) & session.removeAttribute(key):设置/获取/删除会话属 性;在整个会话范围内都可以对这些属性进行操作

9.2 会话的监听器:

会话监听器用于监听会话创建、过期及停止事件

img

9.3 关于SessionDao:

img

具体的一个体系: 可以实现最后的EnterpriseCacheSessionDao

img

来一个示例,有需要自己配

img

img

img

img

img

img

10 . RememberMe:

Shiro 提供了记住我(RememberMe)的功能,比如访问如淘宝 等一些网站时,关闭了浏览器,下次再打开时还是能记住你是谁, 下次访问时无需再登录即可访问,基本流程如下:

• 1、首先在登录页面选中 RememberMe 然后登录成功;如果是 浏览器登录,一般会把 RememberMe 的Cookie 写到客户端并 保存下来;

• 2、关闭浏览器再重新打开;会发现浏览器还是记住你的;

• 3、访问一般的网页服务器端还是知道你是谁,且能正常访问;

• 4、但是比如我们访问淘宝时,如果要查看我的订单或进行支付 时,此时还是需要再进行身份认证的,以确保当前用户还是你。

subject.isAuthenticated() 表示用户进行了身份验证登录的, 即使有 Subject.login 进行了登录; • subject.isRemembered():表示用户是通过记住我登录的, 此时可能并不是真正的你(如你的朋友使用你的电脑,或者 你的cookie 被窃取)在访问的 • 两者二选一,即 subject.isAuthenticated()==true,则 subject.isRemembered()==false;反之一样。

实现:

如果要自己做RememeberMe,需要在登录之前这样创建Token: UsernamePasswordToken(用户名,密码,是否记住我),且调用 UsernamePasswordToken 的:token.setRememberMe(true); 方法

img

建议:

访问一般网页:如个人在主页之类的,我们使用user 拦截 器即可,user 拦截器只要用户登录 (isRemembered() || isAuthenticated())过即可访问成功; • 访问特殊网页:如我的订单,提交订单页面,我们使用 authc 拦截器即可,authc 拦截器会判断用户是否是通过 Subject.login(isAuthenticated()==true)登录的,如 果是才放行,否则会跳转到登录页面叫你重新登录。

11. 会话验证调度器

最后Shiro 提供了会话验证调度器,用于定期的验证会话是否 已过期,如果过期将停止会话 • 出于性能考虑,一般情况下都是获取会话时来验证会话是 否过期并停止会话的;但是如在 web 环境中,如果用户不 主动退出是不知道会话是否过期的,因此需要定期的检测 会话是否过期,Shiro 提供了会话验证调度器 SessionValidationScheduler • Shiro 也提供了使用Quartz会话验证调度器: QuartzSessionValidationScheduler

整合六:(JWT)

开始之前我们先了解两个概念:

认证(authentication):

在程序中认证要做的事情主要是搞明白访问者是谁,他有没有在我们系统中注册过?他是不是我们系统中的用户?如果是,那么这个用户有没有被我们加入黑名单!这是认证要做的事情。举个例子:进一个旅游景区首先要买票吧,要是你手里没有票在检票口会被检票大叔/大妈直接拦住,不让你进去,如果你手里有票,大叔还会看一眼你票是不是今天买的,有没有过期啊……如果你有票并且一切正常,那么你进可以进去了。这就是一个认证的过程!

授权(authorization):

授权的作用主要是验证你有没有访问某项资源的权利!我们都知道在MySQL数据库中,root用户可以随便增删改查各个库的数据,但是普通用户可能就只有查看到权利,而没有删除和修改的权利。在程序中也一样,有一个很常见的需求:用户A可以访问和并修改一个页面,对于用户B可能就只有访问这个页面的权利,而不能修改!

shiro和jwt的简单介绍:

jwt:JSON Web Token,由请求头、请求体和签名3部分组成,它主要用来认证。
shiro是一个轻量级的安全框架,可以使用注解快速的实现权限管理。他主要用来授权。

实现认证和授权的整体思路:

客户端访问服务端,服务端对请求进行认证,主要包括用户名和密码是否正确,如果认证成功会给客户端颁发一个凭证token,后面客户端再访问服务端的时候都要携带这个token,如果不携带或者token被篡改,都会认证失败!如果token认证成功,客户端访问资源,那么此时服务端还会去认证该用户是否有访问此资源的权利!如果此用户没有访问这个资源的权利,同样会访问失败!

数据库表设计

权限的认证和校验采用RBAC模型,总共有5张表,分别为user、user_role、role、role_permission和permission,
其中user_role和role_permission为中间表,user表和role表示一对多的关系,
role表和permission表也是一对多的关系。

代码实现

首先是登录的代码,即是用来认证的代码,当用户认证成功后会返回一个token给客户端。认证失败客户端将不能获取token!

登录

@RestController
@Slf4j
public class LoginController {
    @Resource
    private UserService userService;

    @PostMapping("/login")
    public ResponseBean login(@RequestBody JSONObject requestJson) {
        log.info("用户登录");
        String username = requestJson.getString("username");
        String password = requestJson.getString("password");

        if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
            throw new RuntimeException("用户名和密码不可以为空!");
        }

        // 从数据库中根据用户名查找该用户信息
        UserDTO userDTO = userService.getByUseName(username);

        // 获取盐
        String salt = userDTO.getSalt();
        // 原密码加密(通过username + salt作为盐)
        String encodedPassword = ShiroKit.md5(password, username + salt);
        if (null != userDTO && userDTO.getPassword().equals(encodedPassword)) {
            return new ResponseBean(200, "Login success", JWTUtil.sign(username, encodedPassword));
        } else {
            throw new UnauthorizedException("用户名或者密码错误!");
        }
    }

}

JWT生成token(附带验证的一些工具类)

public class JWTUtil {
    final static Logger logger = LogManager.getLogger(JWTUtil.class);

    /**
     * 过期时间30分钟
     */
    private static final long EXPIRE_TIME = 30 * 60 * 1000;

    /**
     * 校验token是否正确
     *
     * @param token  密钥
     * @param secret 用户的密码
     * @return 是否正确
     */
    public static boolean verify(String token, String username, String secret) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(secret);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("username", username)
                    .build();
            verifier.verify(token);
            return true;
        } catch (Exception exception) {
            return false;
        }
    }

    /**
     * 获得token中的信息无需secret解密也能获得
     *
     * @return token中包含的用户名
     */
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /**
     * 生成签名,30min后过期
     *
     * @param username 用户名
     * @param secret   用户的密码
     * @return 加密的token
     */
    public static String sign(String username, String secret) {
        try {
            Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
            //使用用户自己的密码充当加密密钥
            Algorithm algorithm = Algorithm.HMAC256(secret);
            // 附带username信息
            //  建造者模式
            String jwtString = JWT.create()
                    .withClaim("username", username)
                    .withExpiresAt(date)
                    .sign(algorithm);
            logger.debug(String.format("JWT:%s", jwtString));
            return jwtString;
        } catch (UnsupportedEncodingException e) {
            return null;
        }
    }
}

过滤器

将登录的请求排除之外,然后其他的请求,在请求之前都必须走认证过程:

public class JWTFilter extends BasicHttpAuthenticationFilter {

    private Logger LOGGER = LoggerFactory.getLogger(this.getClass());

    /**
     * 判断用户是否想要登入。
     * true:是要登录
     * 检测header里面是否包含Authorization字段即可
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        String authorization = req.getHeader("Authorization");
        return authorization != null;
    }

    /**
     *
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response)  throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String authorization = httpServletRequest.getHeader("Authorization");

        JWTToken token = new JWTToken(authorization);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(token);
        // 如果没有抛出异常则代表登入成功,返回true
        return true;
    }

    /**
     * 这里我们详细说明下为什么最终返回的都是true,即允许访问
     * 例如我们提供一个地址 GET /article
     * 登入用户和游客看到的内容是不同的
     * 如果在这里返回了false,请求会被直接拦截,用户看不到任何东西
     * 所以我们在这里返回true,Controller中可以通过 subject.isAuthenticated() 来判断用户是否登入
     * 如果有些资源只有登入用户才能访问,我们只需要在方法上面加上 @RequiresAuthentication 注解即可
     * 但是这样做有一个缺点,就是不能够对GET,POST等请求进行分别过滤鉴权(因为我们重写了官方的方法),但实际上对应用影响不大
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if (isLoginAttempt(request, response)) {
            try {
                executeLogin(request, response);
            } catch (Exception e) {
                response401(request, response);
            }
        }
        return true;
    }

    /**
     * 对跨域提供支持
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

    /**
     * 将非法请求跳转到 /401
     */
    private void response401(ServletRequest req, ServletResponse resp) {
        try {
            HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
            httpServletResponse.sendRedirect("/401");
        } catch (IOException e) {
            LOGGER.error(e.getMessage());
        }
    }
}

自定义MyRealm

自定义权限和身份认证逻辑:

@Configuration
public class MyRealm extends AuthorizingRealm {
    @Resource
    private UserService userService;


    /**
     * 必须重写此方法,不然Shiro会报错
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }

    /**
     * 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = JWTUtil.getUsername(principals.toString());
        // 根据用户名获取用户角色 权限信息
        UserDTO user = userService.getByUseName(username);
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

        // 获取角色
        List<String> roleNames = getRoleNameList(user.getRoleList());
        for (String roleName : roleNames) {
            simpleAuthorizationInfo.addRole(roleName);
        }

        // 获取权限
        List<String> permissions = getPermissionList(user.getPermissionList());
        simpleAuthorizationInfo.addStringPermissions(new HashSet<>(permissions));
        return simpleAuthorizationInfo;
    }

    /**
     * 获取权限
     *
     * @param permissionList
     * @return
     */
    private List<String> getPermissionList(List<Permission> permissionList) {
        List<String> permissions = new ArrayList<>(permissionList.size());
        for (Permission permission : permissionList) {
            if (StringUtils.isNotBlank(permission.getPerCode())) {
                permissions.add(permission.getPerCode());
            }
        }
        return permissions;
    }

    /**
     * 获取角色名称
     *
     * @param roleList
     * @return
     */
    private List<String> getRoleNameList(List<Role> roleList) {
        List<String> roleNames = new ArrayList<>(roleList.size());
        for (Role role : roleList) {
            roleNames.add(role.getName());
        }
        return roleNames;
    }

    /**
     * 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        String token = (String) auth.getCredentials();
        // 解密获得username,用于和数据库进行对比
        String username = JWTUtil.getUsername(token);
        if (username == null) {
            throw new AuthenticationException("token invalid");
        }

        UserDTO userBean = userService.getByUseName(username);
        if (userBean == null) {
            throw new AuthenticationException("User didn't existed!");
        }

        if (!JWTUtil.verify(token, username, userBean.getPassword())) {
            throw new AuthenticationException("Username or password error");
        }

        return new SimpleAuthenticationInfo(token, token, "my_realm");
    }
}

MyRealm的解释

自定义Realm是实现权限的关键,MyRealm继承Shiro框架的AuthorizingRealm抽象类,然后重写doGetAuthorizationInfo方法,在这个方法中,我们先是从客户端携带的token中取出用户名,然后根据用户名在数据库中查出该用户所拥有的角色和角色所拥有的权限,然后分别将角色和权限放到SimpleAuthorizationInfo对象中。如下:

 // 获取角色
        List<String> roleNames = getRoleNameList(user.getRoleList());
        for (String roleName : roleNames) {
            simpleAuthorizationInfo.addRole(roleName);
        }

        // 获取权限
        List<String> permissions = getPermissionList(user.getPermissionList());
        simpleAuthorizationInfo.addStringPermissions(new HashSet<>(permissions));

Shiro注解

1.RequiresAuthentication:

  • 使用该注解标注的类,实例,方法在访问或调用时,当前Subject必须在当前session中已经过认证。

2.RequiresGuest:

  • 使用该注解标注的类,实例,方法在访问或调用时,当前Subject可以是“gust”身份,不需要经过认证或者在原先的session中存在记录。

3.RequiresPermissions:

  • 当前Subject需要拥有某些特定的权限时,才能执行被该注解标注的方法。如果当前Subject不具有这样的权限,则方法不会被执行。

4.RequiresRoles:

  • 当前Subject必须拥有所有指定的角色时,才能访问被该注解标注的方法。如果当天Subject不同时拥有所有指定角色,则方法不会执行还会抛出AuthorizationException异常。

5.RequiresUser:

  • 当前Subject必须是应用的用户,才能访问或调用被该注解标注的类,实例,方法。

我们常用的就是RequiresRoles和RequiresPermissions。
这些注解是有使用顺序的:

RequiresRoles-->RequiresPermissions-->RequiresAuthentication-->RequiresUser-->RequiresGuest
如果有个多个注解的话,前面的通过了会继续检查后面的,若不通过则直接返回

@RequiresRoles(value = {"admin","guest"},logical = Logical.OR)
   这个注解的作用是携带的token中的角色必须是admin、guest中的一个
   如果把logical = Logical.OR改成logical = Logical.AND,那么这个注解的作用就变成角色必须同时包含admin和guest了.
   
@RequiresPermissions(logical = Logical.OR, value = {"user:view", "user:edit"})@RequiresRoles类似,权限的的值放在value中,值之间的关系用Logical.OR和Logical.AND控制

代码测试

登录

ç™»å
¥èŽ·å–token

未携带token访问资源

未携带token访问资源

携带token访问

携带token访问成功

需要guest角色但却是admin的token

需要guest角色但却是admin的token

guest权限guest的token

guest权限guest的token

权限不足的情况

权限不足的æƒ
况

有权限访问

有权限访问

项目完整代码

GitHub项目完整代码

参考博文

整合七:(配置)

shiro框架—shiro配置介绍(一)

项目已分享到GitHub上,如果需要的可以看下,springboot+shiro项目Git下载地址

shiro在springboot项目中的配置步骤

1、引入依赖

首先shiro的应用,引入的依赖仅仅只有一个,即下边这个。

 <dependency>
   <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-all</artifactId>
      <version>1.3.2</version>
  </dependency>

2、shiro在springboot项目中的位置:

以下是shiro在springboot项目中的位置:

这里写图片描述

主要的文件有四个ShiroConfig 、RetryLimitHashedCredentialsMatcher 、UserRealm 、MShiroFilterFactoryBean 。在这里 `UserOAuthMatcher 没有用到,其实它跟RetryLimitHashedCredentialsMatcher 是一样的意思,都是实现的同一个shiro的接口,用哪一个都可以,我在下边的链接里就不放进UserOAuthMatcher 了。

3、以上配置文件的主要功能:

(1) shiroConfig
  关于shiroConfig 的配置主要内容,其实就是下图的这些:

这里写图片描述

以下是shiroConfig 文件的全部配置。其他的配置文件都是围绕这个文件展开工作的。

package microservice.fpzj.shiro;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.authc.AnonymousFilter;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.web.filter.DelegatingFilterProxy;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
    @Bean
    public FilterRegistrationBean filterRegistrationBean() {
        FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
        filterRegistration.setFilter(new DelegatingFilterProxy("shiroFilter")); 
        filterRegistration.setEnabled(true);
        filterRegistration.addUrlPatterns("/*"); //过滤规则,即所有的请求
        filterRegistration.setDispatcherTypes(DispatcherType.REQUEST);
        return filterRegistration;
    }

    /**
     * 这个即是上边调用的shiroFilter过滤器,也就是shiro配置的过滤器
     * @return
     */
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(){
        /**
        *MShiroFilterFactoryBean指向自定义过滤器,自定义过滤器对js/css等忽
        *略
        **/
        ShiroFilterFactoryBean bean = new MShiroFilterFactoryBean(); 
        bean.setSecurityManager(securityManager());
        bean.setLoginUrl("/login");
        bean.setUnauthorizedUrl("/unauthor");
        Map<String, Filter>filters = new LinkedHashMap<>();
//      filters.put("perms", urlPermissionsFilter());
        filters.put("anon", new AnonymousFilter());
        bean.setFilters(filters);
        //shiro配置过滤规则少量的话可以用hashMap,数量多了要用LinkedHashMap,保证有序,原因未知
        Map<String, String> chains = new LinkedHashMap<>();
        chains.put("/login", "anon");
        chains.put("/unauthor", "anon");
        chains.put("/logout", "anon");
        chains.put("/weblogin", "anon");
        chains.put("/**", "authc");
        bean.setFilterChainDefinitionMap(chains);
        return bean;
    }

    /**
     * @see org.apache.shiro.mgt.SecurityManager
     * @return
     */
    @Bean(name="securityManager")
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(userRealm());
        //manager.setCacheManager(cacheManager());
        manager.setSessionManager(defaultWebSessionManager());
        return manager;
    }

    /**
     * @see DefaultWebSessionManager
     * @return
     */
    @Bean(name="sessionManager")
    public DefaultWebSessionManager defaultWebSessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        //sessionManager.setCacheManager(cacheManager());
        sessionManager.setGlobalSessionTimeout(1800000);
        sessionManager.setDeleteInvalidSessions(true);
        sessionManager.setSessionValidationSchedulerEnabled(true);
        sessionManager.setDeleteInvalidSessions(true);
        sessionManager.setSessionIdCookie(getSessionIdCookie());
        return sessionManager;
    }
    /**
     * 给shiro的sessionId默认的JSSESSIONID名字改掉
     * @return
     */
    @Bean(name="sessionIdCookie")
    public SimpleCookie getSessionIdCookie(){
        SimpleCookie simpleCookie = new SimpleCookie("webcookie");
        /**
         * HttpOnly标志的引入是为了防止设置了该标志的cookie被JavaScript读取,
         * 但事实证明设置了这种cookie在某些浏览器中却能被JavaScript覆盖,
         * 可被攻击者利用来发动session fixation攻击
         */
        simpleCookie.setHttpOnly(true);
        /**
         * 设置浏览器cookie过期时间,如果不设置默认为-1,表示关闭浏览器即过期
         * cookie的单位为秒 比如60*60为1小时
         */
        simpleCookie.setMaxAge(-1);
        return simpleCookie;
    }

    /**
     * @see UserRealm--->AuthorizingRealm
     * @return
     */
    @Bean
    @DependsOn(value="lifecycleBeanPostProcessor")
    public UserRealm userRealm() {
        UserRealm userRealm = new UserRealm();
        userRealm.setCredentialsMatcher(credentialsMatcher());
        //userRealm.setCacheManager(cacheManager());
        return userRealm;
    }
    @Bean(name="credentialsMatcher")
    public CredentialsMatcher credentialsMatcher() {
        return new RetryLimitHashedCredentialsMatcher();
    }


    /*@Bean
    public EhCacheManager cacheManager() {
        EhCacheManager cacheManager = new EhCacheManager();
        cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
        return cacheManager;
    }*/

    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }
}

下边针对于shiroConfig 文件中的配置,一一说明。
  首先该文件的第一个配置为FilterRegistrationBean 。 springboot注入过滤器有多种方式,一种是最简单的@WebFilter注解,一种就是下边的这种,写一个FilterRegistrationBean,然后将自定义过滤器set进去,下边是通过DelegatingFilterProxy 代理的方式,注入容器中名字为shiroFilter 的过滤器,最后设置过滤器的规则。

@Bean
    public FilterRegistrationBean filterRegistrationBean() {
        FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
        /**
        *DelegatingFilterProxy做的事情是代理Filter的方法,从application
        *context里获得bean,从下边可以理解到,它是将容器中名字为shiroFilter
        *的过滤器加入到过滤器注册bean中
        **/
        filterRegistration.setFilter(new DelegatingFilterProxy("shiroFilter")); 
        filterRegistration.setEnabled(true);
        filterRegistration.addUrlPatterns("/*"); //过滤规则,即所有的请求
        filterRegistration.setDispatcherTypes(DispatcherType.REQUEST);
        return filterRegistration;
    }

既然上边注入的是名字为shiroFilter 的过滤器,那下边就是shiroFilter 的配置

/**
     * 这个即是上边调用的shiroFilter过滤器,也就是shiro配置的过滤器
     * @return
     */
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(){
        /**
        *MShiroFilterFactoryBean指向自定义过滤器,自定义过滤器对js/css等忽
        *略
        **/
        ShiroFilterFactoryBean bean = new MShiroFilterFactoryBean(); 
        bean.setSecurityManager(securityManager());
        bean.setLoginUrl("/login");
        bean.setUnauthorizedUrl("/unauthor");
        Map<String, Filter>filters = new LinkedHashMap<>();
//      filters.put("perms", urlPermissionsFilter());
        /**
        * shiro自己的过滤器,anon,表示不拦截的路径,authc,表示拦截的路径
        **/
        filters.put("anon", new AnonymousFilter());
        bean.setFilters(filters);
        /*
        *shiro配置过滤规则少量的话可以用hashMap,数量多了要用
        *LinkedHashMap,保证有序,原因未知。
        *,anon,表示不拦截的路径,authc,表示拦截的路径。匹配时,首先匹配
        *anon的,然后最后匹配authc
        **/
        Map<String, String> chains = new LinkedHashMap<>();
        chains.put("/login", "anon");
        chains.put("/unauthor", "anon");
        chains.put("/logout", "anon");
        chains.put("/weblogin", "anon");
        chains.put("/**", "authc"); 
        bean.setFilterChainDefinitionMap(chains);
        return bean;
    }

以上的shiroFilter 配置中,又引入了我们的第二个配置文件,名字为MShiroFilterFactoryBean ,该类继承了ShiroFilterFactoryBean 类,通过名字,应该大体能知道,它是shiro的过滤器工厂类,而我们的MShiroFilterFactoryBean 类就是一个自定义的shiro过滤器,为什么要自己写一个过滤器呢?
  在当前的shiro框架中,无法拦截那种.js 、.css 、.html 、.jsp 等等带有. 的请求路径,对于前三种这种静态资源,我们可以不纳入shiro拦截,即可以在不登录的情况下访问成功,但是我们现在有这样的需求,即对.jsp 也要拦截起来,那就需要自定义shiro过滤器了,即现在MShiroFilterFactoryBean 类存在的意义。关于该类的解释,在后边再说,我们继续shiroConfig 文件的介绍。
  在上边的配置中,其实就是自定义了一个shiro过滤器,然后对其进行了一些操作,其中bean.setLoginUrl(“/login”) 是在项目启动后,如果没有登录的情况下,会被shiro强制请求的路径,即为/login ;
  另外,bean.setSecurityManager(securityManager()); 这句的配置,即引入设置shiro的控制中心,即securityManager ,安全管理器;即所有与安全有关的操作都会与securityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器;关于subject ,你就理解为是每一个访问系统的用户对象即可,所有的访问用户的情况都是一种subject 的体现,它们又统一被securityManager 管理,这个在第一篇里已经说过。
  通过上边的shiroFilter 的配置之后,然后再看securityManager。

/**
     * @see org.apache.shiro.mgt.SecurityManager
     * @return
     */
    @Bean(name="securityManager")
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(userRealm());
        //manager.setCacheManager(cacheManager());
        manager.setSessionManager(defaultWebSessionManager());
        return manager;
    }

以上又引入了我们的第三个配置文件,即UserRealm 文件,改文件又引出了我们的一个概念,Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。
  在这里的UserRealm 类继承于AuthorizingRealm ,该类的作用其实有用户密码验证、权限授权等。这个也会在后边贴出来,先继续讲shiroConfig 文件。
  通过上边的manager.setSessionManager(defaultWebSessionManager()); 然后引入下边的配置

/**
     * @see DefaultWebSessionManager
     * @return
     */
    @Bean(name="sessionManager")
    public DefaultWebSessionManager defaultWebSessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        //sessionManager.setCacheManager(cacheManager());
        sessionManager.setGlobalSessionTimeout(1800000);
        sessionManager.setDeleteInvalidSessions(true);
        sessionManager.setSessionValidationSchedulerEnabled(true);
        sessionManager.setDeleteInvalidSessions(true);
        sessionManager.setSessionIdCookie(getSessionIdCookie());
        return sessionManager;
    }

上边的配置,主要是对session的配置,比如,超时时间的设置等,基本上都是跟session相关的配置。另外,上边还有getSessionIdCookie() 方法的引用,众所周知,浏览器与后台系统交互的方式,是以后台存储session,然后将该session对应key,以字符串的形式返给浏览器,并在浏览器中以cookie 的形式记录起来,方便后续的访问,如果浏览器丢失了这个cookie,那就会失去与后台系统的联系,必须重新登录,才能重新再生成这个cookie。而getSessionIdCookie() 方法,即是对cookie在浏览器那里的名字的定义,如下:

/**
     * 给shiro的sessionId默认的JSSESSIONID名字改掉
     * @return
     */
    @Bean(name="sessionIdCookie")
    public SimpleCookie getSessionIdCookie(){
        SimpleCookie simpleCookie = new SimpleCookie("webcookie");
        /**
         * HttpOnly标志的引入是为了防止设置了该标志的cookie被JavaScript读取,
         * 但事实证明设置了这种cookie在某些浏览器中却能被JavaScript覆盖,
         * 可被攻击者利用来发动session fixation攻击
         */
        simpleCookie.setHttpOnly(true);
        /**
         * 设置浏览器cookie过期时间,如果不设置默认为-1,表示关闭浏览器即过期
         * cookie的单位为秒 比如60*60为1小时
         */
        simpleCookie.setMaxAge(-1);
        return simpleCookie;
    }

以上配置起的名字为webcookie ,这个webcookie 就是浏览器那边存储后台传入过来的cookie 的key。整个的大体流程,我理解的如下:

这里写图片描述

以后再请求,只要浏览器没有清除cookie ,上边关于session 的超时时间没有超时,就可以正常访问系统。之所以,这里要给sessionid 起一个名字webcookie 这是防止浏览器访问多个系统的时候,恰巧碰上两个系统在浏览器那边存储sessionid 对应的key正好相同,即session污染 ,造成访问系统出现问题。如果不设置,shiro默认的sessionid 在前端浏览器的名字为cookie 。
  关于session污染 的异常,我遇到的是下边这个:

 Found 'sid' cookie value [1a22b751-0542-4e74-a8e7-59942692f6ae]
22:13:37 DEBUG net.sf.ehcache.Cache - mx-master-SessionCache cache - Miss
22:13:37 DEBUG o.a.shiro.mgt.DefaultSecurityManager - Resolved SubjectContext context session is invalid.  Ignoring and creating an anonymous (session-less) Subject instance.
org.apache.shiro.session.UnknownSessionException: There is no session with id [1a22b751-0542-4e74-a8e7-59942692f6ae]

如果出现There is no session with id 的异常,不出意外的话,就是上边的配置有问题,需要有cookie 的配置,相关的文章,你可以看这一篇一个项目两个web模块会导致shiro的session污染 ,可以得到解释。
  继续shiroconfig 文件的配置,然后再后边就是如下配置:

    @Bean
    @DependsOn(value="lifecycleBeanPostProcessor")
    public UserRealm userRealm() {
        UserRealm userRealm = new UserRealm();
        userRealm.setCredentialsMatcher(credentialsMatcher());
        //userRealm.setCacheManager(cacheManager());
        return userRealm;
    }
    @Bean(name="credentialsMatcher")
    public CredentialsMatcher credentialsMatcher() {
        return new RetryLimitHashedCredentialsMatcher();
    }
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

上边有userRealm 类的注入,而在前边securityManager 的配置的时候,它引入了userRealm ,就是在这里对userRealm 类注入的。
另外,上边userRealm 方法中设置了一个credentialsMatcher() ,该方法对应的就是new RetryLimitHashedCredentialsMatcher() 类,这里就引入了我们第四个配置文件RetryLimitHashedCredentialsMatcher 类,该类,继承于HashedCredentialsMatcher 类,而HashedCredentialsMatcher 你如果往上找,其实就是实现了CredentialsMatcher 接口,所以这里注入的时候,可以以自定义的RetryLimitHashedCredentialsMatcher 类注入成CredentialsMatcher ,该类的功能主要是将用户输入的密码与查询到的密码进行比较,也就是密码比较器。
这个shiroConfig 类写的有点多,我理解的也有点不足,有些片面,如果有不对的地方,请读者帮我指正,我及时改过来。
另外提到的另外三个配置文件,先不写了,放到下一篇吧,今天写的有点多了。先贴上shiro的这四个配置文件的下载地址shiro的配

下一篇文章shiro框架—shiro配置介绍(二)

shiro框架—shiro配置介绍(二)

接上一篇shiro框架—shiro配置介绍(一)

项目已分享到GitHub上,如果需要的可以看下,springboot+shiro项目Git下载地址

关于shiro在springboot的配置,共有四个基本配置文件主要的文件有四个ShiroConfig 、RetryLimitHashedCredentialsMatcher 、UserRealm 、MShiroFilterFactoryBean。
  因为上一篇仅仅写完了shiroconfig 类的配置,虽然在上一篇最下边,我已经附上了所有文件的下载链接,但是我觉得还是要写一下剩下的三个配置文件,写一下我对shiro配置的理解。下边是剩余三个配置文件,即RetryLimitHashedCredentialsMatcher 、UserRealm 、MShiroFilterFactoryBean 的配置详情。

1.UserRealm 类的配置内容

具体代码如下:

package microservice.fpzj.shiro;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import microservice.fpzj.core.models.Permission;
import microservice.fpzj.core.models.Role;
import microservice.fpzj.core.models.User;
import microservice.fpzj.service.jwt.UserService;

/**
 * 验证用户登录
 * 
 * @author Administrator
 */
@Component("userRealm")
public class UserRealm extends AuthorizingRealm {   
    @Autowired
    private UserService userService;

    public UserRealm() {
        setName("UserRealm");
        // 采用MD5加密
        setCredentialsMatcher(new HashedCredentialsMatcher("md5"));
    }

    /**
    *权限资源角色配置,如果在jsp里加入了shiro的标签配置,将会在访问那个jsp页
    * 面的时候触发这个方法,这里先不考虑这个方法的逻辑。
    **/
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        User user = (User)principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        return info;
    }

    //登录验证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken upt = (UsernamePasswordToken) token;
        char[] passwords = upt.getPassword();
        String pwd = new String(passwords);
        if(!StringUtils.isEmpty(pwd)){ //如果密码不是空,表示是本地登录
            User user = this.userService.findUserByUserName(upt.getUsername());
            if(user == null){
                throw new AuthenticationException("no_user");
            }
            boolean res =true;
            for(Role r:user.getRoles()){
                if(r.getSiteid()==2){
                    res = false;
                    break;
                }
            }
            if(res){
                throw new AuthenticationException("no_permission");
            }
            if(user!=null  && !user.isDeleted()){
                SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(
                        user, //用户对象
                        user.getPasswd(), //密码
                        ByteSource.Util.bytes(user.getUserid()+user.getUsername()),//salt=username+salt
                        getName());  //realm name
                return info;
            }       
            return null;//getAuthenticationInfoMethod(upt.getUsername());
        }else{
          /**
          *如果密码是空,表示是远程登录,从username位置获得token,
          *暂时不考虑这种情况,这里只走上边if里边
          **/
            String username = userService.getUsernameFromToken(upt.getUsername());
            return getAuthenticationInfoMethod(username);       
        }

    }
    public AuthenticationInfo getAuthenticationInfoMethod(String username){
        User user = userService.findUserByUserName(username);
        if(user == null){
            throw new AuthenticationException("no_user");
        }
        if(user != null){
            boolean res =true;
            for(Role r:user.getRoles()){
                if(r.getSiteid()==2){
                    res = false;
                    break;
                }
            }
            if(res){
                throw new AuthenticationException("no_permission");
            }

            SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(
                    user, //用户对象
                    user.getPasswd(), //密码
                    ByteSource.Util.bytes(user.getUserid()+user.getUsername()),//salt=username+salt
                    getName());  //realm name
            return info;
        }
        return null;
    }
}

以上主要看登录验证方法doGetAuthenticationInfo 在该方法里,通过从自己写的userService 接口,先根据用户名查询出用户对象,即调用findUserByUserName 方法,获取用户对象下的角色roles,通过判断用户下的角色集合中,是否含有siteId 为2的,如果含有siteId 为2的,则让通过登录,如果没有当前siteId 的角色,则登录失败,站点siteId 表示系统类别,2在我们多系统里为业务系统,还有门户系统、后台系统等。
  siteId我在第一篇里已经写了,shiro框架—关于shiro框架的简单介绍及用户表的建立维护

2.RetryLimitHashedCredentialsMatcher 类的配置内容

具体代码如下:

package microservice.fpzj.shiro;


import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.springframework.beans.factory.annotation.Autowired;
import microservice.fpzj.core.models.User;
import microservice.fpzj.service.jwt.UserService;

/**
 * 
 * @author ZML
 *
 */
public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {
    @Autowired
    private UserService userService;

    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        if(token instanceof UsernamePasswordToken){
            UsernamePasswordToken userToken = (UsernamePasswordToken) token;
            User user = userService.findUserByUserName(userToken.getUsername());
            char[] passwordchars =userToken.getPassword();
            String password = new String(passwordchars);
            if("".equals(password)){ //token字符串登录
                boolean flg = userService.verifyToken(userToken.getUsername());
                return flg;
            }else{ //账号密码登录
                boolean res = userService.passwordVerify(password,user.getPasswd(),user.getUserid()+user.getUsername());
                return res;
            }
        }
        return false;
    }
}

上一篇已经说过,RetryLimitHashedCredentialsMatcher类,继承于HashedCredentialsMatcher 类,对于HashedCredentialsMatcher 类,你如果往上找,就会发现,其实就是实现了CredentialsMatcher 接口,最后又在shiroConfig 配置文件中,将该类注入到我们的spring容器中供UserRealm 调用,加入到shiro 框架里,这样就可以扩展我们自定义的这个RetryLimitHashedCredentialsMatcher 类,该类的功能主要是将用户输入的密码与查询到的密码进行比较,也就是密码比较器。
  上边方法doCredentialsMatch 的逻辑里,有verifyToken 这样的方法,其实这个是针对于那种没有账号密码,只有token 字符串的密钥登录的方式。这里先不说这一种。
  userService 里是我们自己提前写好的一系列针对于用户验证的接口,因为shiro 不会去维护用户、权限、角色这些东西,需要我们自己维护,关于建表的那些步骤,我在第一篇里已经写了,shiro框架—关于shiro框架的简单介绍及用户表的建立维护,比如这里的passwordVerify 方法 ,通过名字可以理解,是验证密码的接口,这些会在下一篇写出来。

3.MShiroFilterFactoryBean 类的配置内容

还记得在上一篇文章shiro框架—shiro配置介绍(一) 里,提到过 MShiroFilterFactoryBean 继承于 ShiroFilterFactoryBean 类,是我们自己自定义的shiroFilter ,该类在shiroConfig 里注入到spring 容器中,另外,因为shiro 的拦截是对所有请求进行拦截,关于.js 、.css 、.html 、.png等静态资源的请求,应该是不要拦截的,所以我们需要在自定义shiroFilter 里对这些静态资源进行过滤,对我们项目来说,这就是这个MShiroFilterFactoryBean 的存在的意义。
  具体配置如下:

package microservice.fpzj.shiro;

import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.mgt.FilterChainManager;
import org.apache.shiro.web.filter.mgt.FilterChainResolver;
import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver;
import org.apache.shiro.web.mgt.WebSecurityManager;
import org.apache.shiro.web.servlet.AbstractShiroFilter;
import org.springframework.beans.factory.BeanInitializationException;
/**
 * 自定义shiro过滤规则
 * @author Administrator
 *
 */
public class MShiroFilterFactoryBean extends ShiroFilterFactoryBean{
//   对ShiroFilter来说,需要直接忽略的请求
    private Set<String> ignoreExt;
    public MShiroFilterFactoryBean() {
        super();
        ignoreExt = new HashSet<>();
        ignoreExt.add(".jsp");
//        ignoreExt.add(".png");
    }

    @Override
    protected AbstractShiroFilter createInstance() throws Exception {
        org.apache.shiro.mgt.SecurityManager securityManager = getSecurityManager();
        if (securityManager == null) {
            String msg = "SecurityManager property must be set.";
            throw new BeanInitializationException(msg);
        }

        if (!(securityManager instanceof WebSecurityManager)) {
            String msg = "The security manager does not implement the WebSecurityManager interface.";
            throw new BeanInitializationException(msg);
        }
        FilterChainManager manager = createFilterChainManager();
        PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
        chainResolver.setFilterChainManager(manager);
        return new MSpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
    }

    private final class MSpringShiroFilter extends AbstractShiroFilter {
        protected MSpringShiroFilter(WebSecurityManager webSecurityManager, FilterChainResolver resolver) {
            super();
            if (webSecurityManager == null) {
                throw new IllegalArgumentException("WebSecurityManager property cannot be null.");
            }
            setSecurityManager(webSecurityManager);
            if (resolver != null) {
                setFilterChainResolver(resolver);
            }
        }

        @Override
        protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse,
                FilterChain chain) throws ServletException, IOException {
            HttpServletRequest request = (HttpServletRequest)servletRequest;
            String str = request.getRequestURI().toLowerCase();
            // 因为ShiroFilter 拦截所有请求(在上面我们配置了urlPattern 为 * ,当然你也可以在那里精确的添加要处理的路径,这样就不需要这个类了),而在每次请求里面都做了session的读取和更新访问时间等操作,这样在集群部署session共享的情况下,数量级的加大了处理量负载。
            // 所以我们这里将一些能忽略的请求忽略掉。
            // 当然如果你的集群系统使用了动静分离处理,静态资料的请求不会到Filter这个层面,便可以忽略。
            boolean flag = true;
            int idx = 0;
            if(( idx = str.indexOf(".")) > 0 ){
                str = str.substring(idx);
                /**
                *如果不是jsp则为false,即除了.jsp,其他的都不需要拦截
                **/
                if(!ignoreExt.contains(str.toLowerCase())){ 
                    flag = false;
                }
            }
            if(flag){
                super.doFilterInternal(servletRequest, servletResponse, chain);
            }else{
                chain.doFilter(servletRequest, servletResponse);
            }
        }

    }
}

通过以上的配置,就会将所有不是.jsp 的又带有.资源全部放开,即,没有登录的情况下,可以访问到,其他带有. 的资源则全部被shiro拦截。

4.注意点

以上的配置文件中,你会看到有很多的userService 中的方法,我没有贴出来,后边我也都会贴出来,但是,其实这样的逻辑基本都是数据库查询的逻辑。

比如:下面的这个是根据用户名获取用户对象的方法,因为用户名是唯一的,这个应该很容易写出来,就是根据用户名字段去匹配用户表的一条对象记录。

User user = userService.findUserByUserName(upt.getUsername())

下面的这个是验证账号密码的方法。

boolean flag = userService.passwordVerify(password,user.getPasswd(),user.getUserid()+user.getUsername())

下面的这个是根据token字符串获取用户名,token是一种jwt字符串,是一种很长的字符串,这里主要用于那种没有用户名,只有一个令牌token字符串,通过这个token字符串,来到后台验证的时候,需要通过这个字符串转换成用户名,然后再根据用户名,调用上边的方法获取用户对象。这里先不考虑这种的登录,只考虑正常的那种账号密码的登录方式。

String username = userService.getUsernameFromToken(upt.getUsername())

下面的这个是验证客户端传入的token字符串是否有效的接口。同样这里不考虑这种的登录方式。

boolean flag = userService.verifyToken(userToken.getUsername())

整合八:(用户名和密码)

接上一篇文章shiro框架—shiro配置介绍(二)
  项目已分享到GitHub上,如果需要的可以看下,springboot+shiro项目Git下载地址

关于上一篇中,写出了关于shiro在springboot项目的配置步骤,还有几个简单的注意点没有说到。

1、用户名保证唯一性

这个应该很容易理解,对于一个系统来说,用户名是唯一的,不可重复,如果我们在注册用户时,比如随便起了一个用户名doudouchong 这样的用户。合理的方式是,在输入注册用户名后,页面应该马上调用一个接口,即查询当前用户名是否占用,如果占用应该提示已注册。在这样的前提下,就能保证用户名的唯一了。

2、密码入库要用暗文

在注册用户时,比如我们将注册的用户doudouchong 密码为123 ,这样的信息保存到数据库用户表中,但是合理的情况下,你不能把这个密码明文123 保存到数据库中,你应该把暗文保存到数据库中,密码加密后的暗文,可以有多种方法来做到,比如我们项目中是用的下边的这个类:

package microservice.fpzj.core.util;

import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.util.ByteSource;

public class PasswordUtil {
    private String algorithmName = "md5";   //指定散列算法为MD5,还有别的算法如:SHA256、SHA1、SHA512
    private int hashIterations = 2;     //散列迭代次数 md5(md5(pwd)): new Md5Hash(pwd, salt, 2).toString()
    public void setAlgorithmName(String algorithmName) {
        this.algorithmName = algorithmName;
    }
    public void setHashIterations(int hashIterations) {
        this.hashIterations = hashIterations;
    }
    //加密:输入明文得到密文
    public String encodePassword(String pwd, String salt) {
        //user.setSalt(randomNumberGenerator.nextBytes().toHex());
        String newPassword = new SimpleHash(
                algorithmName,
                pwd,
                ByteSource.Util.bytes(salt),
                hashIterations).toHex();

        return newPassword;
    } 
    public boolean verifyPassword(String targetPassword, String pwd, String salt){
         String newPassword = this.encodePassword(targetPassword, salt);
         if(newPassword.equals(pwd)){
             return true;
         }else{
             return false;
         }
    }

}

以上的 encodePassword 方法,即是加密密码明文的方法,该方法的参数除了明文密码,还有salt ,这个是盐,通过这个盐,可以对密码进一步加密,而这个盐,这里其实使用的是userid ,是通过UUID 获取的一个随机的字符串,作为用户表记录主键userid 的值,然后它们两个通过encodePassword 方法生成密码暗文,大体意思如下图所示:
这里写图片描述

通过传入123 和生成的uuid 值d9970477fb2349b984b1c98b8e559a91 两个参数,调用encodePassword 方法,即生成了密码暗文b7331bcddf29abef3a079ea0cb678f0e ,然后将该暗文作为用户表中的密码值存储到用户表中即可。

3、密码验证的逻辑

由于密码存入到用户表中是暗文,所以在验证中,不要忘记首先将用户输入的密码123 转换成暗文,然后再雨数据库用户表中存储的密码值进行比较。方法也是调用第二步中的encodePassword ,但是因为这个方法还需要盐,即salt ,而这个salt 值又来自于用户表中当前用户名的主键userid值,所以,还要根据用户名查询到用户对象,取出userid 值,作为salt ,然后调用encodePassword 方法,这样才能转换成真正的密码暗文,如果salt 不对,生成的密码暗文肯定是不对的。

整合九:(登录退出)

项目已分享到GitHub上,如果需要的可以看下,springboot+shiro项目Git下载地址

在我前几篇文章里有shiro配置的文件下载包,下载后里边有四个配置文件ShiroConfig、RetryLimitHashedCredentialsMatcher 、UserRealm 、MShiroFilterFactoryBean。这四个配置文件,在前边几篇文章里,已经一一写明,还有一个文件,即LoginCheckController 类,这个里边有关于shiro 框架下提供的登录、退出的接口。

具体配置如下:

package microservice.fpzj.shiro.controller;
import microservice.fpzj.control.base.BaseController;
import microservice.fpzj.core.models.Role;
import microservice.fpzj.core.models.User;
import microservice.fpzj.service.jwt.UserService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import javax.servlet.http.HttpServletRequest;

@Controller //如果用RestController,比如login.html就会只显示值,必须用controller注解才可以显示页面
public class LoginCheckController extends BaseController{
    private static Logger logger = LoggerFactory.getLogger(LoginCheckController.class);
    @Autowired
    private UserService userService;
    /**
     * 用户登录验证入口
     * 1、首次访问全部过滤到/index请求下,因为没有token,进入shiro验证会不通过,则会重定向到远程进行登录
     * 2、远程登录成功后,根据该处给出的redirect地址,带上token重定向本方法,进行重新验证。
     * @param
     * @return
     */
    @RequestMapping(value="/weblogin",method=RequestMethod.GET)
    public String ssoLoginForm(String httptoken) throws Exception {
        boolean res=userService.verifyHttpToken(httptoken);
        if(!res){
            return "profession/views/login";
        }
        String token = userService.getContentFromHttpToken(httptoken);
        UsernamePasswordToken upt = new UsernamePasswordToken(token,"");
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(upt);
        } catch (Exception e) {
            return "profession/views/login";
        }
        Session session = subject.getSession();
        session.setAttribute("token", token);
        String username = userService.getUsernameFromToken(token);
        User user = userService.findUserByUserName(username);
        user.setPasswd("");
        session.setAttribute("sessionUser", user);
        return "redirect:home";
    }
    /**
     * 跳转本地登录页面
     */
    @RequestMapping(value="/login",method=RequestMethod.GET)
    public String login(HttpServletRequest request){
        return "profession/views/login";
    }
    /**
     * 本地登录页面账户密码提交验证
     * @param request
     * @return
     */
    @RequestMapping(value="/login",method=RequestMethod.POST)
    @ResponseBody
    public Object loginForm(HttpServletRequest request) throws Exception {
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        UsernamePasswordToken upt = new UsernamePasswordToken(username,password);
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(upt);
        } catch (Exception e) {
            if("no_user".equals(e.getMessage())){
                return addResultMapMsg(false,"当前用户不存在");
            }
            if("no_permission".equals(e.getMessage())){
                return addResultMapMsg(false,"该用户没有当前系统的访问权限");
            }
            return addResultMapMsg(false,"账号密码错误");
        }
        Session session = subject.getSession();
        String createToken = userService.createTokenAndSave(username);
        session.setAttribute("token", createToken);
        User user = userService.findUserByUserName(username);
        for (Role r : user.getRoles()) {
            if (r.getSiteid() == 2) { //取出业务系统角色,将该角色赋值给user对象下的role属性 20170923 lsf
                user.setRole(r);
                break;
            }
        }
        user.setPasswd("");
        session.setAttribute("sessionUser", user);
        return addResultMapMsg(true,"登录成功");
    }
    /**
     * 登录首页
     * @return
     */
    @RequestMapping(value={"/home","/"},method=RequestMethod.GET)
    public Object home(){
        return "profession/views/index";
    }

    /**
     * 本地登出页面
     * @param redirectAttributes
     * @return
     */
    @RequestMapping(value="/logout",method=RequestMethod.GET)  
    public String logout(RedirectAttributes redirectAttributes ){ 
        //使用权限管理工具进行用户的退出,跳出登录,给出提示信息
        Subject subject = SecurityUtils.getSubject();
        if (subject.isAuthenticated()) {  
            subject.logout();  
        }
        return "profession/views/login";
    }
}

以上有个login 方法,这是一个GET 请求方法,是shiroConfig 类中配置的未登录的情况下跳转的第一个接口,意思是,只要没有登录就会跳转到这个接口。
  另外还有个loginForm 方法,是一个POST 请求方法,该方法是登录页面输入账号密码后,请求的登录验证接口,这地方主要有这样一个地方需要注意一下,如下:

UsernamePasswordToken upt = new UsernamePasswordToken(username,password);
Subject subject = SecurityUtils.getSubject();
try {
            subject.login(upt);
        } catch (Exception e) {
            return "profession/views/login";
        }

以上的UsernamePasswordToken 是用来存放用户名密码的对象,
另外SecurityUtils.getSubject() ,这个特别重要,是shiro 框架中获取的当前访问的用户对象,一个客户端将会对应一个subject,最后就是subject.login(upt) 该方法,就开始进入到shiro 框架里验证账号密码了,从这里再往后走的逻辑图具体如下:
这里写图片描述

还有一个地方,即退出的logout 方法,因为退出后,需要清除shiro 框架里记录的用户信息,比如subject 对象,以及其中的session 对象等等,那么就需要该方法里的写法:

Subject subject = SecurityUtils.getSubject();
        if (subject.isAuthenticated()) {  
            subject.logout();  
        }
        return "profession/views/login";

以上的方法subject.logout() 即退出,并清除subject

整合十:(按钮权限控制)

shiro框架里按钮的权限控制

项目已分享到GitHub上,如果需要的可以看下,springboot+shiro项目Git下载地址

通过前几篇的文章里,写了关于数据库建表、shiro框架在springboot中的配置步骤、以及登录时的shiro验证的接口。
因为对于功能中系统 、模块 、菜单 的类型,都统一根据条件sql查询返回给前端,由前端控制起来,而剩下的操作 类型,也就是按钮,是通过shiro的标签来维护的,下面就写一下关于shiro标签的使用方法。

1、jsp页面中引入标签

在需要做按钮控制的jsp 页面中加入如下标签,最上边第二行,加上就行:

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

2、在要控制的按钮上加上如下配置

<shiro:hasPermission name="user_authorize"><button id="user_limits">用户授权</button></shiro:hasPermission>

对于上边的name 属性中的值user_authorize 其实就是一个字符串,随便起的,该功能在页面上的展示如下:

这里写图片描述

上图中当前用户下只有一个角色测试角色2 只要对该角色授权时,勾选上该按钮用户授权 这个按钮,就可以了。注意在添加该功能信息的时候,不要选错了资源类型 ,一定要选为操作 类型。

3、验证的逻辑

当在jsp 中做好了上边两步以后,然后找到userRealm 类,在doGetAuthorizationInfo 方法中添加如下内容:

//表示根据用户身份获取授权信息
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        User user = (User)principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        Map<String,Object> argsMap = new HashMap<String,Object>();
        argsMap.put("username",user.getUsername());
        argsMap.put("siteid","1");
        argsMap.put("funcType","4");
        List<SysFunc> sysFuncList = sysFuncService.selectSysFuncByUserName(argsMap); //获取所有按钮的功能记录
        if(sysFuncList != null){
            Set<String> permissonSet = new HashSet<String>();
            for(SysFunc func:sysFuncList){
                if(!StringUtils.isEmpty(func.getFunc_Url())){
                    permissonSet.add(func.getFunc_Url());
                };
            }
            info.setStringPermissions(permissonSet);
        }
        return info;
    }

在该方法里,主要就是根据用户名username、系统标识siteid 、功能类型funcType为按钮的,查询出所有的符合条件的功能列表。并且将功能列表中的func_url 添加到 shiro中的 setStringPermissions 集合里。
  对于上边这个方法什么时候触发,我觉得有必要说一下,它的触发时机不是用户登录后触发的!!!而是当点击jsp 页面的时候,只要该页面有shiro 的上边两处标签配置,都会先走这个方法的。也就是说每点击一次带有配置的页面,就会触发上边的接口一次。

比如我给当前用户下的测试角色2 配置了当前的用户授权 按钮权限,那在点击用户管理页面,就会先触发上边的方法,查询出属于当前用户、当前系统属于按钮的功能,并且把里边的func_url 字符串值集合赋给shiro ,回到页面后,在<shiro:hasPermission name=“user_authorize”> 就会从那个集合里查是否有相应的字符串,有的话就会显示该按钮,没有则不显示。

4.异常注意点

对于按钮的配置,比较简单,但也容易出错,我碰到过一个错误,写在了另一篇文章里,地址是shiro标签页点击报错: No SecurityManager accessible to the calling code…

整合十一:(多站点登录)

关于shiro框架中用户多站点登录的标识字段的位置

上一篇文章shiro框架—关于项目按钮权限控制的配置要点
  项目已分享到GitHub上,如果需要的可以看下,springboot+shiro项目Git下载地址

shiro框架—关于shiro框架的简单介绍及用户表的建立维护文章里,我写过建表的内容,其中提到了功能表角色表 有关siteId的放置位置。

首先再回顾下,我们整个用户、角色、功能配置上所要建立的表:

  • 用户表 (SYS_USER)
  • 用户角色关联表 (SYS_USER_ROLE_R)
  • 角色表 (SYS_ROLE)
  • 角色功能关联表 (SYS_FUNC_ROLE_R)
  • 功能表 (SYS_FUNC)

通过以上表实现一个用户多站点登录的功能,但是关于siteId 放置的位置还需要说明一下

1、不能放到用户表

毫无疑问,无法实现一个用户对应多个站点的功能。

2、可以放到功能表中

放到功能表中是可以实现的,因为用户上又授予多个角色,角色上授予多个功能,这样在用户登录的时候,可以根据用户名,一直关联查询,看看是否当前用户拥有属于当前站点的功能,如果拥有,即可以登录,如果没有,即登录失败。

但是,大家可以看看上图,我们的五张表,如果按照我上边的说的,固然可以实现关联,而且我们自己系统是这样实现的。但是我觉得不好,因为这样做的后果就是,当一个用户登录系统的时候,就先关联查询用户表 、用户角色关联表 、角色表 、角色功能关联表 、功能表五张表,然后通过查询到的所有功能列表中,查找siteId 的值是否与当前站点的值一致,一致即可以登录。这样一个登录功能却匹配了五张表才能实现反馈,我觉得不是很好。

3、可以放到角色表和功能表中

如果只放到角色表中,这样通过用户授予多个站点角色,可以实现用户对应多个角色的功能。但是这个也有个问题,如果siteId只放到角色表,当在某个站点角色授权功能的时候,就必须只能选择属于当前站点的一些功能,至于其他站点的功能就不能选择。如下:

这里写图片描述

如上图,站点系统1 和站点系统2 是两个系统的功能,但是我在给当前属于站点系统1 的角色授权的时候,显示的功能列表却不只是站点系统1 的,这样就感觉挺乱的,最好的结果是,只会显示站点系统1 l里所有的功能,但是因为功能表里没有关于siteid 的信息,即每一条功能信息,无法知道它是属于哪个站点系统的。因此这里的查询会把所有的功能列表都展示出来。
  既然如此,那么再到功能表中,添加一个siteid ,这样的话,上图做筛选的时候,就可以根据角色上的siteid ,查询出只属于当前站点系统 的功能即可。
  即如下图所示:
这里写图片描述

这样做,虽然是两次定义siteId 但是,在用户登录的时候,不用像第2条中的那样,去匹配五张表,这样只需要匹配到角色表,即三张表,即可知道当前用户是否有该系统的登录权限。虽然只是少了两张表,但是角色 数据远远少于功能 数据。因此匹配的数据量也是少了很多的。

所以,我觉得第三种的方式是最好的方式。

Shiro 过滤器:

配置缩写对应的过滤器功能
anonAnonymousFilter指定url可以匿名访问
authcFormAuthenticationFilter指定url需要form表单登录,默认会从请求中获取username、password,rememberMe等参数并尝试登录,如果登录不了就会跳转到loginUrl配置的路径。我们也可以用这个过滤器做默认的登录逻辑,但是一般都是我们自己在控制器写登录逻辑的,自己写的话出错返回的信息都可以定制嘛。
authcBasicBasicHttpAuthenticationFilter指定url需要basic登录
logoutLogoutFilter登出过滤器,配置指定url就可以实现退出功能,非常方便
noSessionCreationNoSessionCreationFilter禁止创建会话
permsPermissionsAuthorizationFilter需要指定权限才能访问
portPortFilter需要指定端口才能访问
restHttpMethodPermissionFilter将http请求方法转化成相应的动词来构造一个权限字符串,这个感觉意义不大,有兴趣自己看源码的注释
rolesRolesAuthorizationFilter需要指定角色才能访问
sslSslFilter需要https请求才能访问
userUserFilter需要已登录或“记住我”的用户才能访问

当 Shiro 被运用到 web 项目时,Shiro 会自动创建一些默认的过滤器对客户端请求进行过滤。比如身份验证、授权等 相关的。默认拦截器可以参考 org.apache.shiro.web.filter.mgt.DefaultFilter中的枚举 拦截器:

以下是 Shiro 提供的过滤器:

过滤器简称对应的 Java 类
anonorg.apache.shiro.web.filter.authc.AnonymousFilter
authcorg.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasicorg.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
permsorg.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
portorg.apache.shiro.web.filter.authz.PortFilter
restorg.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
rolesorg.apache.shiro.web.filter.authz.RolesAuthorizationFilter
sslorg.apache.shiro.web.filter.authz.SslFilter
userorg.apache.shiro.web.filter.authc.UserFilter
logoutorg.apache.shiro.web.filter.authc.LogoutFilter
noSessionCreationorg.apache.shiro.web.filter.session.NoSessionCreationFilter

解释:

  1. /admins/**=anon # 表示该 uri 可以匿名访问
  2. /admins/**=auth # 表示该 uri 需要认证才能访问
  3. /admins/**=authcBasic # 表示该 uri 需要 httpBasic 认证
  4. /admins/**=perms[user:add:] # 表示该 uri 需要认证用户拥有 user:add: 权限才能访问
  5. /admins/**=port[8081] # 表示该 uri 需要使用 8081 端口
  6. /admins/=rest[user] # 相当于 /admins/=perms[user:method],其中,method 表示 get、post、delete 等
  7. /admins/**=roles[admin] # 表示该 uri 需要认证用户拥有 admin 角色才能访问
  8. /admins/**=ssl # 表示该 uri 需要使用 https 协议
  9. /admins/**=user # 表示该 uri 需要认证或通过记住我认证才能访问
  10. /logout=logout # 表示注销,可以当作固定配置

注意:

anon,authcBasic,auchc,user 是认证过滤器。

perms,roles,ssl,rest,port 是授权过滤器。

前面的地址支持Ant风格

img

img img

实现认证和退出:

login.jsp中实现登录

既然要实现认证就需要用户有登录操作,此时我们去修改以下login.jsp

<form action="${pageContext.request.contextPath}/user/login" method="post">
        用户名:<input type="text" name="username" > <br/>
        密码  : <input type="text" name="password"> <br>
        <input type="submit" value="登录">
</form>

我么看到上面的登录请求的url是user/login,这个时候我们就需要一个控制器UserController了

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("user")
public class UserController {
    /**
     * 用户登录
     * @param username
     * @param password
     * @return
     */
    @RequestMapping("login")
    public String login(String username,String password){
        // 获取当前登录用户
        Subject subject = SecurityUtils.getSubject();

        try {
            // 执行登录操作
            subject.login(new UsernamePasswordToken(username,password));
            // 认证通过后直接跳转到index.jsp
            return "redirect:/index.jsp";
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("用户名错误~");
        } catch (IncorrectCredentialsException e) {
            e.printStackTrace();
            System.out.println("密码错误~");
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 如果认证失败仍然回到登录页面
        return "redirect:/login.jsp";
    }
}

index.jsp中实现退出

上面我们说了出了认证还有退出功能也要实现,下面我们在index.jsp中实现

<%--解决页面乱码--%>
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport"
              content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>index</title>
    </head>

    <body>
        <h1>系统主页</h1>
        <a href="${pageContext.request.contextPath}/user/logout">退出用户</a>
        <ul>
            <li><a href="">用户管理</a></li>
            <li><a href="">订单管理</a></li>
        </ul>
    </body>
</html>

同样的我们需要在UserController中实现退出的操作

@RequestMapping("logout")
public String logout(){
    Subject subject = SecurityUtils.getSubject();
    subject.logout();
    // 退出后仍然会到登录页面
    return "redirect:/login.jsp";
}

在CustomerRealm中实现认证

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

/**
 * 自定义realm
 */
public class CustomerRealm extends AuthorizingRealm {
    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 获取当前登录的主题
        String principal = (String) token.getPrincipal();
        // 模拟数据库返回的数据
        if("christy".equals(principal)){
            return new SimpleAuthenticationInfo(principal,"123456",this.getName());
        }
        return null;
    }
}

上面的认证中只要我们输入的用户名是christy,密码123456就可以认证通过进入到主页

ShiroConfiguration

最后还有一步千万不能忘了就是修改ShiroConfiguration

//配置系统受限资源
//配置系统公共资源
Map<String,String> map = new HashMap<String,String>();
map.put("/user/login","anon");  // anon 设置为公共资源,放行要注意anon和authc的顺序
map.put("/index.jsp","authc");  //authc 请求这个资源需要认证和授权

//默认认证界面路径
shiroFilterFactoryBean.setLoginUrl("/login.jsp");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);

测试

以上操作准备完毕后,我们重启项目后进行测试

在这里插å
¥å›¾ç‰‡æè¿°

MD5、Salt的注册流程:

新建register.jsp

<%--解决页面乱码--%>
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport"
              content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>register</title>
    </head>

    <body>
        <h1>用户注册</h1>

        <form action="${pageContext.request.contextPath}/user/register" method="post">
            用户名:<input type="text" name="username" > <br/>
            密码  : <input type="text" name="password"> <br>
            <input type="submit" value="立即注册">
        </form>
    </body>
</html>

新建t_user

DROP TABLE IF EXISTS `t_user`;
create table `t_user` (
	`id` int (11),
	`username` varchar (32),
	`password` varchar (32),
	`salt` varchar (32),
	`age` int (11),
	`email` varchar (32),
	`address` varchar (128)
); 

修改pom.xml

<!-- mybatis plus -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.1</version>
</dependency>

<!-- Druid数据源 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.10</version>
</dependency>

<!-- Mysql -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

修改application.yml

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      driver-class-name: com.mysql.jdbc.Driver
      url: jdbc:mysql://localhost:3306/christy?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=UTC
      username: root
      password: 123456
      # 监控统计拦截的filters
      filters: stat,wall,log4j,config
      # 配置初始化大小/最小/最大
      initial-size: 5
      min-idle: 5
      max-active: 20
      # 获取连接等待超时时间
      max-wait: 60000
      # 间隔多久进行一次检测,检测需要关闭的空闲连接
      time-between-eviction-runs-millis: 60000
      # 一个连接在池中最小生存的时间
      min-evictable-idle-time-millis: 300000
      validation-query: SELECT 'x'
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      # 打开PSCache,并指定每个连接上PSCache的大小。oracle设为true,mysql设为false。分库分表较多推荐设置为false
      pool-prepared-statements: false
      max-pool-prepared-statement-per-connection-size: 20
      
mybatis-plus:
  type-aliases-package: com.christy.shiro.model.entity
  configuration:
    map-underscore-to-camel-case: true

新建User.java

/**
 * @Author Christy
 * @DESC
 * @Date 2020/11/16 15:38
 **/
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("t_user")
@ApiModel("用户实体类")
public class User implements Serializable {
    /** 数据库中设置该字段自增时该注解不能少 **/
    @TableId(type = IdType.AUTO)
    @ApiModelProperty(name = "id", value = "ID 主键")
    private Integer id;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    @ApiModelProperty(name = "username", value = "用户名")
    private String username;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    @ApiModelProperty(name = "password", value = "密码")
    private String password;

    @TableField(fill = FieldFill.INSERT)
    @ApiModelProperty(name = "salt", value = "盐")
    private String salt;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    @ApiModelProperty(name = "age", value = "年龄")
    private Integer age;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    @ApiModelProperty(name = "email", value = "邮箱")
    private String email;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    @ApiModelProperty(name = "address", value = "地址")
    private String address;
}

新建UserMapper.java

package com.christy.shiro.model.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.christy.shiro.model.entity.User;
import org.apache.ibatis.annotations.Mapper;

/**
 * @Author Christy
 * @DESC
 * @Date 2020/11/16 15:49
 **/
@Mapper
public interface UserMapper extends BaseMapper<User> {

}

新建UserService.java、UserServiceImpl.java及相关类

UserService.java


import com.christy.shiro.model.entity.User;

public interface UserService {
    /**
     * 用户注册
     * @param user
     */
    void register(User user);
}

UserServiceImpl.java

package com.christy.shiro.service.impl;

import com.christy.shiro.constants.ShiroConstant;
import com.christy.shiro.model.entity.User;
import com.christy.shiro.model.mapper.UserMapper;
import com.christy.shiro.service.UserService;
import com.christy.shiro.utils.SaltUtil;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;

    @Override
    public void register(User user) {
        // 生成随机盐
        String salt = SaltUtil.getSalt(ShiroConstant.SALT_LENGTH);
        // 保存随机盐
        user.setSalt(salt);
        // 生成密码
        Md5Hash password = new Md5Hash(user.getPassword(), salt, ShiroConstant.HASH_ITERATORS);
        // 保存密码
        user.setPassword(password.toHex());
        userMapper.insert(user);
    }
}

SaltUtil.java

package com.christy.shiro.utils;

import java.util.Random;

/**
 * 用户随机盐生成工具类
 */
public class SaltUtil {
    /**
     * 生成salt的静态方法
     * @param n
     * @return
     */
    public static String getSalt(int n){
        char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890!@#$%^&*()".toCharArray();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < n; i++) {
            char aChar = chars[new Random().nextInt(chars.length)];
            sb.append(aChar);
        }
        return sb.toString();
    }
}

ShiroConstant.java

package com.christy.shiro.constants;

public class ShiroConstant {
    /** 随机盐的位数 **/
    public static final int SALT_LENGTH = 8;
    /** hash的散列次数 **/
    public static final int HASH_ITERATORS = 1024;

}

修改UserController.java

package com.christy.shiro.controller;

import com.christy.shiro.model.entity.User;
import com.christy.shiro.service.UserService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("user")
public class UserController {
    @Autowired
    private UserService userService;
    
    …………省略其他方法…………
    
	/**
     * 用户注册
     * @param user
     * @return
     */
    @RequestMapping("register")
    public String register(User user){
        try {
            userService.register(user);
            return "redirect:/login.jsp";
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "redirect:/register.jsp";
    }
}

修改ShiroConfiguration.java

最后不要忘记修改ShiroConfiguration.java,将register.jsp/user/register配置成公共的可访问资源

    // anon 设置为公共资源,放行要注意anon和authc的顺序
    map.put("/user/register","anon");
    map.put("/register.jsp","anon");

重启项目测试

在这里插å
¥å›¾ç‰‡æè¿°

可以看到我们注册的用户已经顺利保存到数据库,而且密码是经过加密的

MD5、Salt的认证流程:

上面我们完成了基于MD5+Salt的注册流程,保存到数据库的密码都是经过加密处理的,这时候再用最初的简单密码匹配器进行equals方法进行登录显然是不行的了,我们下面来改造一下认证的流程

修改UserService.java、UserServiceImpl.java

UserService.java

package com.christy.shiro.service;

import com.christy.shiro.model.entity.User;

public interface UserService {
    ……省略其他方法……

    /**
     * 根据用户名获取用户
     */
    User findUserByUserName(String userName);
}

UserServiceImpl.java

package com.christy.shiro.service.impl;

import com.christy.shiro.constants.ShiroConstant;
import com.christy.shiro.model.entity.User;
import com.christy.shiro.model.mapper.UserMapper;
import com.christy.shiro.service.UserService;
import com.christy.shiro.utils.SaltUtil;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service("userService")
public class UserServiceImpl implements UserService {
    ……省略其他方法……

    @Override
    public User findUserByUserName(String userName) {
        return userMapper.findUserByUsername(userName);
    }
}

修改CustomerRealm.java及其相关类

CustomerRealm.java

package com.christy.shiro.realm;

import com.christy.shiro.model.entity.User;
import com.christy.shiro.service.UserService;
import com.christy.shiro.utils.ApplicationContextUtil;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.util.ObjectUtils;

/**
 * 自定义realm
 */
public class CustomerRealm extends AuthorizingRealm {
    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 获取当前登录的主题
        String principal = (String) token.getPrincipal();
        // 由于CustomerRealm并没有交由工厂管理,故不能诸如UserService
        UserService userService = (UserService) ApplicationContextUtil.getBean("userService");
        User user = userService.findUserByUserName(principal);
        if(!ObjectUtils.isEmpty(user)){
            return new SimpleAuthenticationInfo(user.getUsername(),user.getPassword(), new CustomerByteSource(user.getSalt()),this.getName());
        }
        return null;
    }
}

ApplicationContextUtil.java

package com.christy.shiro.utils;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class ApplicationContextUtil implements ApplicationContextAware {
    public static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.context = applicationContext;
    }

    /**
     * 根据工厂中的类名获取类实例
     */
    public static Object getBean(String beanName){
        return context.getBean(beanName);
    }
}

CustomerByteSource.java

package com.christy.shiro.configuration.shiro.salt;

import org.apache.shiro.codec.Base64;
import org.apache.shiro.codec.CodecSupport;
import org.apache.shiro.codec.Hex;
import org.apache.shiro.util.ByteSource;

import java.io.File;
import java.io.InputStream;
import java.io.Serializable;
import java.util.Arrays;

//自定义salt实现  实现序列化接口
public class CustomerByteSource implements ByteSource, Serializable {

    private byte[] bytes;
    private String cachedHex;
    private String cachedBase64;

    public CustomerByteSource() {

    }

    public CustomerByteSource(byte[] bytes) {
        this.bytes = bytes;
    }

    public CustomerByteSource(char[] chars) {
        this.bytes = CodecSupport.toBytes(chars);
    }

    public CustomerByteSource(String string) {
        this.bytes = CodecSupport.toBytes(string);
    }

    public CustomerByteSource(ByteSource source) {
        this.bytes = source.getBytes();
    }

    public CustomerByteSource(File file) {
        this.bytes = (new CustomerByteSource.BytesHelper()).getBytes(file);
    }

    public CustomerByteSource(InputStream stream) {
        this.bytes = (new CustomerByteSource.BytesHelper()).getBytes(stream);
    }

    public static boolean isCompatible(Object o) {
        return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;
    }

    public byte[] getBytes() {
        return this.bytes;
    }

    public boolean isEmpty() {
        return this.bytes == null || this.bytes.length == 0;
    }

    public String toHex() {
        if (this.cachedHex == null) {
            this.cachedHex = Hex.encodeToString(this.getBytes());
        }

        return this.cachedHex;
    }

    public String toBase64() {
        if (this.cachedBase64 == null) {
            this.cachedBase64 = Base64.encodeToString(this.getBytes());
        }

        return this.cachedBase64;
    }

    public String toString() {
        return this.toBase64();
    }

    public int hashCode() {
        return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0;
    }

    public boolean equals(Object o) {
        if (o == this) {
            return true;
        } else if (o instanceof ByteSource) {
            ByteSource bs = (ByteSource) o;
            return Arrays.equals(this.getBytes(), bs.getBytes());
        } else {
            return false;
        }
    }

    private static final class BytesHelper extends CodecSupport {
        private BytesHelper() {
        }

        public byte[] getBytes(File file) {
            return this.toBytes(file);
        }

        public byte[] getBytes(InputStream stream) {
            return this.toBytes(stream);
        }
    }
}

修改ShiroConfiguration.java及其相关类:

ShiroConfiguration.java

package com.christy.shiro.configuration;

import com.christy.shiro.constants.ShiroConstant;
import com.christy.shiro.realm.CustomerRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authc.credential.Md5CredentialsMatcher;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

/**
 * Shiro的核心配置类,用来整合shiro框架
 */
@Configuration
public class ShiroConfiguration {

    ……省略其他方法……

    //3.创建自定义realm
    @Bean
    public Realm getRealm(){
        CustomerRealm customerRealm = new CustomerRealm();
        // 设置密码匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        // 设置加密方式
        credentialsMatcher.setHashAlgorithmName(ShiroConstant.HASH_ALGORITHM_NAME.MD5);
        // 设置散列次数
        credentialsMatcher.setHashIterations(ShiroConstant.HASH_ITERATORS);
        customerRealm.setCredentialsMatcher(credentialsMatcher);
        return customerRealm;
    }
}

ShiroConstant.java

package com.christy.shiro.constants;

public class ShiroConstant {
    /** 随机盐的位数 **/
    public static final int SALT_LENGTH = 8;
    /** hash的散列次数 **/
    public static final int HASH_ITERATORS = 1024;
    /** 加密方式 **/
    public interface HASH_ALGORITHM_NAME {
        String MD5 = "MD5";
    }
}

重启项目进行测试

在这里插å
¥å›¾ç‰‡æè¿°

上面我们注册的两个账号都能正常登录,至此基于MD5+Salt的认证流程告一段落

Shiro 缓存:

EhCache实现缓存

shiro提供了缓存管理器,这样在用户第一次认证授权后访问其受限资源的时候就不用每次查询数据库从而达到减轻数据压力的作用,使用shiro的缓存管理器也很简单

修改pom.xml

<!--引入shiro和ehcache-->
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-ehcache</artifactId>
  <version>1.5.3</version>
</dependency>

修改ShiroConfiguration.java

package com.christy.shiro.configuration.shiro;

import com.christy.shiro.constants.ShiroConstant;
import com.christy.shiro.configuration.shiro.realm.CustomerRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

/**
 * Shiro的核心配置类,用来整合shiro框架
 */
@Configuration
public class ShiroConfiguration {

    //1.创建shiroFilter  //负责拦截所有请求
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        //给filter设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);

        //配置系统资源
        Map<String,String> map = new HashMap<String,String>();
        // anon 设置为公共资源,放行要注意anon和authc的顺序
        map.put("/user/login","anon");
        map.put("/user/register","anon");
        map.put("/register.jsp","anon");
        //authc 请求这个资源需要认证和授权
        map.put("/index.jsp","authc");

        //默认认证界面路径
        shiroFilterFactoryBean.setLoginUrl("/login.jsp");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);

        return shiroFilterFactoryBean;
    }

    //2.创建安全管理器
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        //给安全管理器设置
        defaultWebSecurityManager.setRealm(realm);

        return defaultWebSecurityManager;
    }

    //3.创建自定义realm
    @Bean
    public Realm getRealm(){
        CustomerRealm customerRealm = new CustomerRealm();

        // 设置密码匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        // 设置加密方式
        credentialsMatcher.setHashAlgorithmName(ShiroConstant.HASH_ALGORITHM_NAME.MD5);
        // 设置散列次数
        credentialsMatcher.setHashIterations(ShiroConstant.HASH_ITERATORS);
        customerRealm.setCredentialsMatcher(credentialsMatcher);

        // 设置缓存管理器
        customerRealm.setCacheManager(new EhCacheManager());
        // 开启全局缓存
        customerRealm.setCachingEnabled(true);
        // 开启认证缓存并指定缓存名称
        customerRealm.setAuthenticationCachingEnabled(true);
        customerRealm.setAuthenticationCacheName("authenticationCache");
        // 开启授权缓存并指定缓存名称
        customerRealm.setAuthorizationCachingEnabled(true);
        customerRealm.setAuthorizationCacheName("authorizationCache");
        return customerRealm;
    }
}

这样就将EhCache集成进来了,但是shiro的这个缓存是本地缓存,也就是说当程序宕机重启后仍然需要从数据库加载数据,不能实现分布式缓存的功能。下面我们来集成redis来实现缓存

集成Redis实现Shiro缓存:

修改pom.xml

<!-- 引入redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

修改application.yml

spring: 
  redis:
    host: 127.0.0.1
    port: 6379
    password: lml123456
    database: 0

启动redis服务

在这里插å
¥å›¾ç‰‡æè¿°

RedisCacheManager.java与RedisCache.java

package com.christy.shiro.configuration.shiro.cache;

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;

public class RedisCacheManager implements CacheManager {
    @Override
    public <K, V> Cache<K, V> getCache(String cacheName) throws CacheException {
        System.out.println("缓存名称: "+cacheName);
        return new RedisCache<K,V>(cacheName);
    }
}
package com.christy.shiro.configuration.shiro.cache;

import com.christy.shiro.utils.ApplicationContextUtil;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.util.Collection;
import java.util.Set;


public class RedisCache<K,V> implements Cache<K,V> {
    private String cacheName;

    public RedisCache() {
    }

    public RedisCache(String cacheName) {
        this.cacheName = cacheName;
    }

    @Override
    public V get(K k) throws CacheException {
        System.out.println("获取缓存:"+ k);
        return (V) getRedisTemplate().opsForHash().get(this.cacheName,k.toString());
    }

    @Override
    public V put(K k, V v) throws CacheException {
        System.out.println("设置缓存key: "+k+" value:"+v);
        getRedisTemplate().opsForHash().put(this.cacheName,k.toString(),v);
        return null;
    }

    @Override
    public V remove(K k) throws CacheException {
        return (V) getRedisTemplate().opsForHash().delete(this.cacheName,k.toString());
    }

    @Override
    public void clear() throws CacheException {
        getRedisTemplate().delete(this.cacheName);
    }

    @Override
    public int size() {
        return getRedisTemplate().opsForHash().size(this.cacheName).intValue();
    }

    @Override
    public Set<K> keys() {
        return getRedisTemplate().opsForHash().keys(this.cacheName);
    }

    @Override
    public Collection<V> values() {
        return getRedisTemplate().opsForHash().values(this.cacheName);
    }

    private RedisTemplate getRedisTemplate(){
        RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtil.getBean("redisTemplate");
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}

修改ShiroConfiguration.java

这一步主要修改的就是**getRealm()**方法中设置缓存管理器的代码,由EnCacheManager()换成我们自定义的RedisCacheManager()其他的不用动

package com.christy.shiro.configuration.shiro;

import com.christy.shiro.configuration.shiro.cache.RedisCacheManager;
import com.christy.shiro.constants.ShiroConstant;
import com.christy.shiro.configuration.shiro.realm.CustomerRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

/**
 * Shiro的核心配置类,用来整合shiro框架
 */
@Configuration
public class ShiroConfiguration {

    //1.创建shiroFilter  //负责拦截所有请求
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        //给filter设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);

        //配置系统资源
        Map<String,String> map = new HashMap<String,String>();
        // anon 设置为公共资源,放行要注意anon和authc的顺序
        map.put("/user/login","anon");
        map.put("/user/register","anon");
        map.put("/register.jsp","anon");
        //authc 请求这个资源需要认证和授权
        map.put("/index.jsp","authc");

        //默认认证界面路径
        shiroFilterFactoryBean.setLoginUrl("/login.jsp");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);

        return shiroFilterFactoryBean;
    }

    //2.创建安全管理器
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        //给安全管理器设置
        defaultWebSecurityManager.setRealm(realm);

        return defaultWebSecurityManager;
    }

    //3.创建自定义realm
    @Bean
    public Realm getRealm(){
        CustomerRealm customerRealm = new CustomerRealm();

        // 设置密码匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        // 设置加密方式
        credentialsMatcher.setHashAlgorithmName(ShiroConstant.HASH_ALGORITHM_NAME.MD5);
        // 设置散列次数
        credentialsMatcher.setHashIterations(ShiroConstant.HASH_ITERATORS);
        customerRealm.setCredentialsMatcher(credentialsMatcher);

        // 设置缓存管理器
        /*customerRealm.setCacheManager(new EhCacheManager());*/
        customerRealm.setCacheManager(new RedisCacheManager());
        // 开启全局缓存
        customerRealm.setCachingEnabled(true);
        // 开启认证缓存并指定缓存名称
        customerRealm.setAuthenticationCachingEnabled(true);
        customerRealm.setAuthenticationCacheName("authenticationCache");
        // 开启授权缓存并指定缓存名称
        customerRealm.setAuthorizationCachingEnabled(true);
        customerRealm.setAuthorizationCacheName("authorizationCache");
        return customerRealm;
    }
}

重启项目测试

在这里插å
¥å›¾ç‰‡æè¿°

上图可以看到我们用户登录以后用户的认证和授权数据已经缓存到redis了,这个时候即使程序重启,redis中的缓存数据也不会删除,除非用户自己退出登录。

集成图片验证码:

视图层-login.jsp

<%--解决页面乱码--%>
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport"
              content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>login</title>
    </head>

    <body>
        <h1>用户登录</h1>

        <form action="${pageContext.request.contextPath}/user/login" method="post">
            用户名:<input type="text" name="username" > <br/>
            密码  : <input type="text" name="password"> <br>
            验证码: <input type="text" name="verifyCode"><img src="${pageContext.request.contextPath}/user/getImage" alt=""><br>
            <input type="submit" value="登录">
        </form>
    </body>
</html>

工具类-VerifyCodeUtil.java

package com.christy.shiro.utils;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Random;

public class VerifyCodeUtil {
    //使用到Algerian字体,系统里没有的话需要安装字体,字体只显示大写,去掉了1,0,i,o几个容易混淆的字符
    public static final String VERIFY_CODES = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
    private static Random random = new Random();
    
    /**
     * 使用系统默认字符源生成验证码
     * @param verifySize 验证码长度
     * @return
     */
    public static String generateVerifyCode(int verifySize) {
        return generateVerifyCode(verifySize, VERIFY_CODES);
    }

    /**
     * 使用指定源生成验证码
     * @param verifySize 验证码长度
     * @param sources    验证码字符源
     * @return
     */
    public static String generateVerifyCode(int verifySize, String sources) {
        if (sources == null || sources.length() == 0) {
            sources = VERIFY_CODES;
        }
        int codesLen = sources.length();
        Random rand = new Random(System.currentTimeMillis());
        StringBuilder verifyCode = new StringBuilder(verifySize);
        for (int i = 0; i < verifySize; i++) {
            verifyCode.append(sources.charAt(rand.nextInt(codesLen - 1)));
        }
        return verifyCode.toString();
    }

    /**
     * 生成随机验证码文件,并返回验证码值
     * @param w
     * @param h
     * @param outputFile
     * @param verifySize
     * @return
     * @throws IOException
     */
    public static String outputVerifyImage(int w, int h, File outputFile, int verifySize) throws IOException {
        String verifyCode = generateVerifyCode(verifySize);
        outputImage(w, h, outputFile, verifyCode);
        return verifyCode;
    }

    /**
     * 输出随机验证码图片流,并返回验证码
     * @param w
     * @param h
     * @param os
     * @param verifySize
     * @return
     * @throws IOException
     */
    public static String outputVerifyImage(int w, int h, OutputStream os, int verifySize) throws IOException {
        String verifyCode = generateVerifyCode(verifySize);
        outputImage(w, h, os, verifyCode);
        return verifyCode;
    }

    /**
     * 生成指定验证码图像文件
     * @param w
     * @param h
     * @param outputFile
     * @param code
     * @throws IOException
     */
    public static void outputImage(int w, int h, File outputFile, String code) throws IOException {
        if (outputFile == null) {
            return;
        }
        File dir = outputFile.getParentFile();
        if (!dir.exists()) {
            dir.mkdirs();
        }
        try {
            outputFile.createNewFile();
            FileOutputStream fos = new FileOutputStream(outputFile);
            outputImage(w, h, fos, code);
            fos.close();
        } catch (IOException e) {
            throw e;
        }
    }

    /**
     * 输出指定验证码图片流
     * @param w
     * @param h
     * @param os
     * @param code
     * @throws IOException
     */
    public static void outputImage(int w, int h, OutputStream os, String code) throws IOException {
        int verifySize = code.length();
        BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
        Random rand = new Random();
        Graphics2D g2 = image.createGraphics();
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        Color[] colors = new Color[5];
        Color[] colorSpaces = new Color[]{Color.WHITE, Color.CYAN, Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE, Color.PINK, Color.YELLOW};
        float[] fractions = new float[colors.length];
        for (int i = 0; i < colors.length; i++) {
            colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)];
            fractions[i] = rand.nextFloat();
        }
        Arrays.sort(fractions);

        g2.setColor(Color.GRAY);// 设置边框色
        g2.fillRect(0, 0, w, h);

        Color c = getRandColor(200, 250);
        g2.setColor(c);// 设置背景色
        g2.fillRect(0, 2, w, h - 4);

        //绘制干扰线
        Random random = new Random();
        g2.setColor(getRandColor(160, 200));// 设置线条的颜色
        for (int i = 0; i < 20; i++) {
            int x = random.nextInt(w - 1);
            int y = random.nextInt(h - 1);
            int xl = random.nextInt(6) + 1;
            int yl = random.nextInt(12) + 1;
            g2.drawLine(x, y, x + xl + 40, y + yl + 20);
        }

        // 添加噪点
        float yawpRate = 0.05f;// 噪声率
        int area = (int) (yawpRate * w * h);
        for (int i = 0; i < area; i++) {
            int x = random.nextInt(w);
            int y = random.nextInt(h);
            int rgb = getRandomIntColor();
            image.setRGB(x, y, rgb);
        }

        shear(g2, w, h, c);// 使图片扭曲

        g2.setColor(getRandColor(100, 160));
        int fontSize = h - 4;
        Font font = new Font("Algerian", Font.ITALIC, fontSize);
        g2.setFont(font);
        char[] chars = code.toCharArray();
        for (int i = 0; i < verifySize; i++) {
            AffineTransform affine = new AffineTransform();
            affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1), (w / verifySize) * i + fontSize / 2, h / 2);
            g2.setTransform(affine);
            g2.drawChars(chars, i, 1, ((w - 10) / verifySize) * i + 5, h / 2 + fontSize / 2 - 10);
        }

        g2.dispose();
        ImageIO.write(image, "jpg", os);
    }

    private static Color getRandColor(int fc, int bc) {
        if (fc > 255)
            fc = 255;
        if (bc > 255)
            bc = 255;
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return new Color(r, g, b);
    }

    private static int getRandomIntColor() {
        int[] rgb = getRandomRgb();
        int color = 0;
        for (int c : rgb) {
            color = color << 8;
            color = color | c;
        }
        return color;
    }

    private static int[] getRandomRgb() {
        int[] rgb = new int[3];
        for (int i = 0; i < 3; i++) {
            rgb[i] = random.nextInt(255);
        }
        return rgb;
    }

    private static void shear(Graphics g, int w1, int h1, Color color) {
        shearX(g, w1, h1, color);
        shearY(g, w1, h1, color);
    }

    private static void shearX(Graphics g, int w1, int h1, Color color) {
        int period = random.nextInt(2);

        boolean borderGap = true;
        int frames = 1;
        int phase = random.nextInt(2);

        for (int i = 0; i < h1; i++) {
            double d = (double) (period >> 1) * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames);
            g.copyArea(0, i, w1, 1, (int) d, 0);
            if (borderGap) {
                g.setColor(color);
                g.drawLine((int) d, i, 0, i);
                g.drawLine((int) d + w1, i, w1, i);
            }
        }
    }

    private static void shearY(Graphics g, int w1, int h1, Color color) {
        int period = random.nextInt(40) + 10; // 50;

        boolean borderGap = true;
        int frames = 20;
        int phase = 7;
        for (int i = 0; i < w1; i++) {
            double d = (double) (period >> 1) * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames);
            g.copyArea(i, 0, 1, h1, 0, (int) d);
            if (borderGap) {
                g.setColor(color);
                g.drawLine(i, (int) d, i, 0);
                g.drawLine(i, (int) d + h1, i, h1);
            }
        }
    }

    public static void main(String[] args) throws IOException {
        //获取验证码
        String s = generateVerifyCode(4);
        //将验证码放入图片中
        outputImage(260, 60, new File(""), s);
        System.out.println(s);
    }
}

Controller层

UserController.java中有两处变动,其一是需要一个生成验证码的方法并输出到页面;其二是要修改认证的流程,先进行验证码的校验,验证码校验通过以后才可以进行Shiro的认证

package com.christy.shiro.controller;

import com.christy.shiro.model.entity.User;
import com.christy.shiro.service.UserService;
import com.christy.shiro.utils.VerifyCodeUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

@Controller
@RequestMapping("user")
@Api(tags = "用户管理Controller")
public class UserController {
    @Autowired
    private UserService userService;
    
    /** 其他方法省略 **/

    /**
     * 用户登录
     * @param username
     * @param password
     * @return
     */
    @RequestMapping("login")
    public String login(String username,String password, String verifyCode,HttpSession session){
        // 校验验证码
        String verifyCodes = (String) session.getAttribute("verifyCode");
        // 获取当前登录用户
        Subject subject = SecurityUtils.getSubject();

        try {
            if(verifyCodes.equalsIgnoreCase(verifyCode)){
                subject.login(new UsernamePasswordToken(username,password));
                return "redirect:/index.jsp";
            } else {
                throw new RuntimeException("验证码错误");
            }
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("用户名错误~");
        } catch (IncorrectCredentialsException e) {
            e.printStackTrace();
            System.out.println("密码错误~");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "redirect:/login.jsp";
    }

    @RequestMapping("getImage")
    public void getImage(HttpSession session, HttpServletResponse response) throws IOException {
        //生成验证码
        String verifyCode = VerifyCodeUtil.generateVerifyCode(4);
        //验证码放入session
        session.setAttribute("verifyCode",verifyCode);
        //验证码存入图片
        ServletOutputStream os = response.getOutputStream();
        response.setContentType("image/png");
        VerifyCodeUtil.outputImage(180,40,os,verifyCode);
    }
}

ShiroConfiguration.java

最后一定要记得做,就是将生成验证码的请求URL放行

map.put("/user/getImage","anon");

重启项目测试

在这里插å
¥å›¾ç‰‡æè¿°

上面可以看到不输入验证码或者输错验证码是登录不进去的,到此验证码我们也成功实现了。

yl = random.nextInt(12) + 1;
g2.drawLine(x, y, x + xl + 40, y + yl + 20);
}

    // 添加噪点
    float yawpRate = 0.05f;// 噪声率
    int area = (int) (yawpRate * w * h);
    for (int i = 0; i < area; i++) {
        int x = random.nextInt(w);
        int y = random.nextInt(h);
        int rgb = getRandomIntColor();
        image.setRGB(x, y, rgb);
    }

    shear(g2, w, h, c);// 使图片扭曲

    g2.setColor(getRandColor(100, 160));
    int fontSize = h - 4;
    Font font = new Font("Algerian", Font.ITALIC, fontSize);
    g2.setFont(font);
    char[] chars = code.toCharArray();
    for (int i = 0; i < verifySize; i++) {
        AffineTransform affine = new AffineTransform();
        affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1), (w / verifySize) * i + fontSize / 2, h / 2);
        g2.setTransform(affine);
        g2.drawChars(chars, i, 1, ((w - 10) / verifySize) * i + 5, h / 2 + fontSize / 2 - 10);
    }

    g2.dispose();
    ImageIO.write(image, "jpg", os);
}

private static Color getRandColor(int fc, int bc) {
    if (fc > 255)
        fc = 255;
    if (bc > 255)
        bc = 255;
    int r = fc + random.nextInt(bc - fc);
    int g = fc + random.nextInt(bc - fc);
    int b = fc + random.nextInt(bc - fc);
    return new Color(r, g, b);
}

private static int getRandomIntColor() {
    int[] rgb = getRandomRgb();
    int color = 0;
    for (int c : rgb) {
        color = color << 8;
        color = color | c;
    }
    return color;
}

private static int[] getRandomRgb() {
    int[] rgb = new int[3];
    for (int i = 0; i < 3; i++) {
        rgb[i] = random.nextInt(255);
    }
    return rgb;
}

private static void shear(Graphics g, int w1, int h1, Color color) {
    shearX(g, w1, h1, color);
    shearY(g, w1, h1, color);
}

private static void shearX(Graphics g, int w1, int h1, Color color) {
    int period = random.nextInt(2);

    boolean borderGap = true;
    int frames = 1;
    int phase = random.nextInt(2);

    for (int i = 0; i < h1; i++) {
        double d = (double) (period >> 1) * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames);
        g.copyArea(0, i, w1, 1, (int) d, 0);
        if (borderGap) {
            g.setColor(color);
            g.drawLine((int) d, i, 0, i);
            g.drawLine((int) d + w1, i, w1, i);
        }
    }
}

private static void shearY(Graphics g, int w1, int h1, Color color) {
    int period = random.nextInt(40) + 10; // 50;

    boolean borderGap = true;
    int frames = 20;
    int phase = 7;
    for (int i = 0; i < w1; i++) {
        double d = (double) (period >> 1) * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames);
        g.copyArea(i, 0, 1, h1, 0, (int) d);
        if (borderGap) {
            g.setColor(color);
            g.drawLine(i, (int) d, i, 0);
            g.drawLine(i, (int) d + h1, i, h1);
        }
    }
}

public static void main(String[] args) throws IOException {
    //获取验证码
    String s = generateVerifyCode(4);
    //将验证码放入图片中
    outputImage(260, 60, new File(""), s);
    System.out.println(s);
}

}


**Controller层**

**UserController.java**中有两处变动,其一是需要一个生成验证码的方法并输出到页面;其二是要修改认证的流程,先进行验证码的校验,验证码校验通过以后才可以进行Shiro的认证

```java
package com.christy.shiro.controller;

import com.christy.shiro.model.entity.User;
import com.christy.shiro.service.UserService;
import com.christy.shiro.utils.VerifyCodeUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

@Controller
@RequestMapping("user")
@Api(tags = "用户管理Controller")
public class UserController {
    @Autowired
    private UserService userService;
    
    /** 其他方法省略 **/

    /**
     * 用户登录
     * @param username
     * @param password
     * @return
     */
    @RequestMapping("login")
    public String login(String username,String password, String verifyCode,HttpSession session){
        // 校验验证码
        String verifyCodes = (String) session.getAttribute("verifyCode");
        // 获取当前登录用户
        Subject subject = SecurityUtils.getSubject();

        try {
            if(verifyCodes.equalsIgnoreCase(verifyCode)){
                subject.login(new UsernamePasswordToken(username,password));
                return "redirect:/index.jsp";
            } else {
                throw new RuntimeException("验证码错误");
            }
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("用户名错误~");
        } catch (IncorrectCredentialsException e) {
            e.printStackTrace();
            System.out.println("密码错误~");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "redirect:/login.jsp";
    }

    @RequestMapping("getImage")
    public void getImage(HttpSession session, HttpServletResponse response) throws IOException {
        //生成验证码
        String verifyCode = VerifyCodeUtil.generateVerifyCode(4);
        //验证码放入session
        session.setAttribute("verifyCode",verifyCode);
        //验证码存入图片
        ServletOutputStream os = response.getOutputStream();
        response.setContentType("image/png");
        VerifyCodeUtil.outputImage(180,40,os,verifyCode);
    }
}

ShiroConfiguration.java

最后一定要记得做,就是将生成验证码的请求URL放行

map.put("/user/getImage","anon");

重启项目测试

在这里插å
¥å›¾ç‰‡æè¿°

上面可以看到不输入验证码或者输错验证码是登录不进去的,到此验证码我们也成功实现了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值