Shiro 实战教程

1.权限的管理

1.1 什么是权限管理

​ 基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。

​ 权限管理包括用户身份认证授权两部分,简称认证授权。对于需要访问控制的资源用户首先经过身份认证,认证通过后用户具有该资源的访问权限方可访问。

1.2 什么是身份认证

身份认证,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。对于采用指纹等系统,则出示指纹;对于硬件Key等刷卡系统,则需要刷卡。

1.3 什么是授权

授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的


2.什么是shiro

Apache Shiro™ is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications.

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

Shiro是apache旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。


3.shiro的核心架构

3.1 Subject

Subject即主体,外部应用与subject进行交互,subject记录了当前操作用户,将用户的概念理解为当前操作的主体,可能是一个通过浏览器请求的用户,也可能是一个运行的程序。 Subject在shiro中是一个接口,接口中定义了很多认证授权相关的方法,外部程序通过subject进行认证授权,而subject是通过SecurityManager安全管理器进行认证授权

3.2 SecurityManager(安全管理器,最核心)

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

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

3.3 Authenticator

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

3.4 Authorizer

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

3.5 Realm

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

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

3.6 SessionManager

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

3.7 SessionDAO

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

3.8 CacheManager

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

3.9 Cryptography

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


4. shiro中的认证

4.1 认证

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

4.2 shiro中认证的关键对象

  • Subject:主体

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

  • Principal:身份信息

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

  • credential:凭证信息

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

4.3 认证流程

4.4 认证的开发

1. 创建项目并引入依赖
<dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-core</artifactId><version>1.5.3</version>
</dependency>
2. 引入shiro配置文件并加入如下配置

配置文件:.ini 结尾文件,复杂数据格式

用来学习shiro书写我们系统中相关权限数据。整合时用不到

[users]
xiaochen=123
zhangsan=456   等于在系统中定义了两个合法用户

3.开发认证代码
public class TestAuthenticator {public static void main(String[] args) {//创建securityManager安全管理器对象DefaultSecurityManager defaultSecurityManager = new                                                                       DefaultSecurityManager();//给安全管理器设置realmdefaultSecurityManager.setRealm(new IniRealm("classpath:shiro.ini"));//给全局安全工具类设置安全管理器SecurityUtils.setSecurityManager(defaultSecurityManager);//获取主体对象Subject subject = SecurityUtils.getSubject();//创建token令牌UsernamePasswordToken token = new UsernamePasswordToken("xiaochen1",                                                                         "123");try {subject.login(token);//用户登录 没有返回值  认证通过没有任何异常,否则会抛出异常System.out.println("登录成功~~");} catch (UnknownAccountException e) {e.printStackTrace();System.out.println("用户名错误!!");}catch (IncorrectCredentialsException e){e.printStackTrace();System.out.println("密码错误!!!");}}
}
  • DisabledAccountException(帐号被禁用)

  • LockedAccountException(帐号被锁定)

  • ExcessiveAttemptsException(登录失败次数过多)

  • ExpiredCredentialsException(凭证过期)等


上边开发认证代码源码分析得到结论:(封装非常深)

1.最终执行用户名比较 SimpleAccountRealm 中doGetAuthenticationInfo 在这个方法中完成用户名校验

2.最终密码校验在AuthenticatingRealm中 assertCredentialsMatch

public class SimpleAccountRealm extends AuthorizingRealm {

总结:

​ AuthenticatingRealm认证realm doGetAuthenticationInfo

​ AuthorizingRealm 授权realm doGetAuthorizationInfo

4.5 自定义Realm

上边的程序使用的是Shiro自带的IniRealm,IniRealm从ini配置文件中读取用户的信息,大部分情况下需要从系统的数据库中读取用户信息,所以需要自定义realm。

1.shiro提供的Realm

2.根据认证源码认证使用的是SimpleAccountRealm

SimpleAccountRealm的部分源码中有两个方法一个是 认证 一个是 授权,

public class SimpleAccountRealm extends AuthorizingRealm {//.......省略protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {UsernamePasswordToken upToken = (UsernamePasswordToken) token;SimpleAccount account = getUser(upToken.getUsername());if (account != null) {if (account.isLocked()) {throw new LockedAccountException("Account [" + account + "] is locked.");}if (account.isCredentialsExpired()) {String msg = "The credentials for account [" + account + "] are expired";throw new ExpiredCredentialsException(msg);}}return account;}//授权protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {String username = getUsername(principals);USERS_LOCK.readLock().lock();try {return this.users.get(username);} finally {USERS_LOCK.readLock().unlock();}}
}
3.自定义realm
//自定义realm实现,将认证/授权数据的来源转为数据库的实现
public class CustomerRealm extends AuthorizingRealm {//授权方法@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {return null;}//认证方法@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {String principal = (String) token.getPrincipal();System.out.println(principal);//能够获取到tocken中的用户名if("xiaochen".equals(principal)){//参数1:返回数据库中正确的用户名 参数2:返回数据库中正确的密码  参数3:提供当前realm的名字 this.getName();return new                                                                             SimpleAuthenticationInfo(principal,"123",this.getName());}return null;}
}
4.使用自定义Realm认证
public class TestAuthenticatorCusttomerRealm {public static void main(String[] args) {//创建securityManagerDefaultSecurityManager defaultSecurityManager = new                                                                        DefaultSecurityManager();//IniRealm realm = new IniRealm("classpath:shiro.ini");//设置为自定义realm获取认证数据defaultSecurityManager.setRealm(new CustomerRealm());//将安装工具类中设置默认安全管理器SecurityUtils.setSecurityManager(defaultSecurityManager);//获取主体对象Subject subject = SecurityUtils.getSubject();//创建token令牌UsernamePasswordToken token = new UsernamePasswordToken("xiaochen",                                                                          "123");try {subject.login(token);//用户登录System.out.println("登录成功~~");} catch (UnknownAccountException e) {e.printStackTrace();System.out.println("用户名错误!!");}catch (IncorrectCredentialsException e){e.printStackTrace();System.out.println("密码错误!!!");}}
}

4.6 使用MD5(加密)和Salt(随机盐)

MD5算法:作用:一般用来加密或者签名(校验和)

​ 特点:算法不可逆,如果内容相同无论执行多少次md5生成结果始终是一致的

​ 123——》feas122121221ddd… 网上很多md5解密工具,使用的是穷举算法:md5只要值一样,执行多少次结果都是一样,网上的将常用的密码都生成好了让你去比较。

​ 生成结果:始终是一个16进制32位长度字符串。

执行流程:

注册时加盐,和密码进行拼接之后存入数据库,并且把盐也存入到数据库中;用户再次登录时先拿着用户名去数据库查找盐,把盐和密码拼接之后再去和数据库中密码比较,并且盐的拼接要和业务层(Service)的拼接顺序一致。

有人攻破数据库会看到盐,还不安全,不能因为防盗门还有可能被盗就不按门。

md5作为签名的应用场景:如何比较a.txt和b.txt这两个文件中的内容是否一致?md5可以对文件的内容进行校验,如果生成的值一样,这两个文件一定是一样的。 例:下载文件时tomcat.zip tomcat.md5点进去可以看到一串字符,当自己下载完成后可以用md5与之比较,一致一定下载正确。

实际应用是将盐和散列后的值存在数据库中,自动realm从数据库取出盐和加密后的值由shiro完成密码校验。

//MD5
public class TestShiroMD5 {public static void main(String[] args) {/*Md5Hash md5Hash=new Md5Hash();md5Hash.setBytes("123".getBytes());String s = md5Hash.toHex();System.out.println(s);*///使用MD5Md5Hash md5Hash = new Md5Hash("123");System.out.println(md5Hash.toHex());//使用MD5+saltMd5Hash md5Hash1 = new Md5Hash("123", "X0*7ps");System.out.println(md5Hash1.toHex());//使用MD5+salt+hash散列   散列次数越多越均匀Md5Hash md5Hash2 = new Md5Hash("123", "X0*7ps", 1024);System.out.println(md5Hash2.toHex());}
}
1.自定义md5+salt的realm
public class CustomerRealm extends AuthorizingRealm {//授权方法@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {return null;}//认证方法@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {String principal = (String) token.getPrincipal();if("xiaochen".equals(principal)){String password = "3c88b338102c1a343bcb88cd3878758e";String salt = "Q4F%";
//参数1:数据库用户名  2:数据库md5+salt之后的密码  3:注册时 的随机盐  4:realm的名字return new SimpleAuthenticationInfo(principal,password, ByteSource.Util.bytes(salt),this.getName());}return null;}
2.使用md5 + salt 认证
public class TestAuthenticatorCusttomerRealm {public static void main(String[] args) {//创建安全管理器DefaultSecurityManager defaultSecurityManager = new                                                                       DefaultSecurityManager();//IniRealm realm = new IniRealm("classpath:shiro.ini");//设置为自定义realm获取认证数据CustomerRealm realm = new CustomerRealm();//设置realm使用hash凭证匹配器HashedCredentialsMatcher credentialsMatcher = new                                                                       HashedCredentialsMatcher();//使用算法credentialsMatcher.setHashAlgorithmName("MD5");//散列次数credentialsMatcher.setHashIterations(1024);//设置散列次数realm.setCredentialsMatcher(credentialsMatcher);defaultSecurityManager.setRealm(realm);//将安装工具类中设置默认安全管理器SecurityUtils.setSecurityManager(defaultSecurityManager);//通过安全工具类获得subjectSubject subject = SecurityUtils.getSubject();//创建token令牌UsernamePasswordToken token = new UsernamePasswordToken("xiaochen",                                                                          "123");try {subject.login(token);//用户登录System.out.println("登录成功~~");} catch (UnknownAccountException e) {e.printStackTrace();System.out.println("用户名错误!!");}catch (IncorrectCredentialsException e){e.printStackTrace();System.out.println("密码错误!!!");}}
}

5. shiro中的授权

5.1 授权

授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。

5.2 关键对象

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

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

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

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

5.3 授权流程

5.4 授权方式

  • 基于角色的访问控制

    • 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用户进行修改
      }

5.5 权限字符串

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

例子:

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

5.6 shiro中授权编程实现方式

  • 编程式

    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需要额外集成!

5.7 开发授权

1.realm的实现
public class CustomerRealm extends AuthorizingRealm {//授权方法@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {String primaryPrincipal = (String) principals.getPrimaryPrincipal();System.out.println("身份信息: " + primaryPrincipal);//根据身份信息 用户名 获取当前用户的角色信息,以及权限信息  查数据库SimpleAuthorizationInfo simpleAuthorizationInfo = new                                                                     SimpleAuthorizationInfo();//将数据库中查询角色信息赋值给权限对象simpleAuthorizationInfo.addRole("admin");simpleAuthorizationInfo.addRole("super");//将数据库中查询权限信息赋值给权限对象simpleAuthorizationInfo.addStringPermission("user:update:01");simpleAuthorizationInfo.addStringPermission("product:*:*");return simpleAuthorizationInfo;}//认证方法@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {String principal = (String) token.getPrincipal();if("xiaochen".equals(principal)){String password = "3c88b338102c1a343bcb88cd3878758e";String salt = "Q4F%";return new SimpleAuthenticationInfo(principal,password, ByteSource.Util.bytes(salt),this.getName());}return null;}}
2.授权
public class TestAuthenticatorCusttomerRealm {public static void main(String[] args) {//创建securityManagerDefaultSecurityManager defaultSecurityManager = new                                                                      DefaultSecurityManager();//IniRealm realm = new IniRealm("classpath:shiro.ini");//设置为自定义realm获取认证数据CustomerRealm customerRealm = new CustomerRealm();//设置md5加密HashedCredentialsMatcher credentialsMatcher = new                                                                       HashedCredentialsMatcher();credentialsMatcher.setHashAlgorithmName("MD5");credentialsMatcher.setHashIterations(1024);//设置散列次数customerRealm.setCredentialsMatcher(credentialsMatcher);defaultSecurityManager.setRealm(customerRealm);//将安装工具类中设置默认安全管理器SecurityUtils.setSecurityManager(defaultSecurityManager);//获取主体对象Subject subject = SecurityUtils.getSubject();//创建token令牌UsernamePasswordToken token = new UsernamePasswordToken("xiaochen",                                                                         "123");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()){//基于角色权限管理boolean admin = subject.hasRole("admin");System.out.println(admin);//基于多角色权限控制System.out.println(subject.hasRoles(Arrays.asList("admin","super")));//基于权限字符串的访问控制   资源标识符:操作:资源类型boolean permitted = subject.isPermitted("product:create:001");System.out.println(permitted);//分别具有哪些权限boolean[] permitted=subject.isPermitted("user:*:01","order:*:10");for(boolean b:permitted){System.out.println(b);}//同时具有哪些权限boolean permittedAll = subject.isPermittedAll("user:*:01",                                                                      "product:*");System.out.println(permittedAll);}}
}

6.整合SpringBoot项目实战

6.0 整合思路

6.1 创建springboot项目

创建webapp目录

导入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>

配置文件配置

spring.application.name=shiro  #给应用起个名字
server.port=8080
server.servlet.context-path=/shirospring.mvc.view.prefix=/
spring.mvc.view.suffix=.jsp

启动访问出错 原因:使用jsp需要配置工作目录


成功:

创建jsp页面技巧: 直接再页面输入!然后按Tab键

访问会出现乱码:

加入:ok

6.2 引入shiro依赖

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

用jsp实现:

//创建配置类
@Configuration
public class ShiroConfig {//1.创建shiroFilter  负责拦截所有请求@Beanpublic 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 请求index.jsp这个资源需要认证和授权shiroFilterFactoryBean.setFilterChainDefinitionMap(map);//默认认证界面路径   访问受限资源会来到默认认证界面,访问index.jsp,会重定向到login.jspshiroFilterFactoryBean.setLoginUrl("/login.jsp");return shiroFilterFactoryBean;}//2.创建安全管理器@Beanpublic DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){DefaultWebSecurityManager defaultWebSecurityManager = new                                                                DefaultWebSecurityManager();//给安全管理器设置defaultWebSecurityManager.setRealm(realm);return defaultWebSecurityManager;}//3.创建自定义realm@Beanpublic Realm getRealm(){CustomerRealm customerRealm=new CustomerRealm();return customerRealm;}
}
public class CustomerRealm extends AuthorizingRealm {@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection                                                                      principals) {return null;}@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken                                         token) throws AuthenticationException {return null;}
}

6.3 配置shiro环境

0.创建配置类

1.配置shiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(SecurityManager securityManager){//创建shiro的filterShiroFilterFactoryBean shiroFilterFactoryBean = new                                                                                ShiroFilterFactoryBean();//注入安全管理器shiroFilterFactoryBean.setSecurityManager(securityManager);return shiroFilterFactoryBean;
}
2.配置WebSecurityManager
@Bean
public DefaultWebSecurityManager getSecurityManager(Realm realm){DefaultWebSecurityManager defaultWebSecurityManager = new                                                                   DefaultWebSecurityManager();defaultWebSecurityManager.setRealm(realm);return defaultWebSecurityManager;
}
3.创建自定义realm

public class CustomerRealm extends AuthorizingRealm {//处理授权@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection                                                                    principals) {return null;}//处理认证@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken                                          token) throws AuthenticationException {return null;}
}
4.配置自定义realm
//创建自定义realm
@Bean
public Realm getRealm(){return new CustomerRealm();
}
5.编写控制器跳转至index.html
@Controller   //thymeleaf默认不能访问,写个控制器
public class IndexController {@RequestMapping("index")public String index(){System.out.println("跳转至主页");return "index";}
}

6.启动springboot应用访问index

  • 注意:

    • 默认在配置好shiro环境后默认环境中没有对项目中任何资源进行权限控制,所有现在项目中所有资源都可以通过路径访问
7.加入权限控制
  • 修改ShiroFilterFactoryBean配置

    //注入安全管理器
    shiroFilterFactoryBean.setSecurityManager(securityManager);
    Map<String,String> map =  new LinkedHashMap<>();
    map.put("/**","authc");
    //配置认证和授权规则
    shiroFilterFactoryBean.setFilterChainDefinitionMap(map);

    • /** 代表拦截项目中一切资源 authc 代表shiro中的一个filter的别名,详细内容看文档的shirofilter列表
8.重启项目访问查看 (重定向到了login.jsp,因为项目是html没有login.jsp所有404)

6.4 常见过滤器

  • 注意: shiro提供和多个默认的过滤器,我们可以用这些过滤器来配置控制指定url的权限:
配置缩写 对应的过滤器 功能
anon AnonymousFilter 指定url可以匿名访问
authc FormAuthenticationFilter 指定url需要form表单登录,默认会从请求中获取usernamepassword,rememberMe等参数并尝试登录,如果登录不了就会跳转到loginUrl配置的路径。我们也可以用这个过滤器做默认的登录逻辑,但是一般都是我们自己在控制器写登录逻辑的,自己写的话出错返回的信息都可以定制嘛。
authcBasic BasicHttpAuthenticationFilter 指定url需要basic登录
logout LogoutFilter 登出过滤器,配置指定url就可以实现退出功能,非常方便
noSessionCreation NoSessionCreationFilter 禁止创建会话
perms PermissionsAuthorizationFilter 需要指定权限才能访问
port PortFilter 需要指定端口才能访问
rest HttpMethodPermissionFilter 将http请求方法转化成相应的动词来构造一个权限字符串,这个感觉意义不大,有兴趣自己看源码的注释
roles RolesAuthorizationFilter 需要指定角色才能访问
ssl SslFilter 需要https请求才能访问
user UserFilter 需要已登录或“记住我”的用户才能访问

6.5 认证实现

1. 在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>
2. 开发controller
@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));return  "redirect:/index.jsp";} catch (UnknownAccountException e) {e.printStackTrace();System.out.println("用户名错误!");}catch (IncorrectCredentialsException e){e.printStackTrace();System.out.println("密码错误!");}return "redirect:/login.jsp";}
}
  • 在认证过程中使用subject.login进行认证
3.开发realm中返回静态数据(未连接数据库)
@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {System.out.println("==========================");String principal = (String) token.getPrincipal();if("xiaochen".equals(principal)){return new SimpleAuthenticationInfo(principal,"123",this.getName());}return null;}
}
4.启动项目以realm中定义静态数据进行认证

  • 认证功能没有md5和随机盐的认证就实现啦

6.6 退出认证

1.开发页面退出连接

2.开发controller
@Controller
@RequestMapping("user")
public class UserController {/*** 退出登录**/@RequestMapping("logout")public String logout(){Subject subject = SecurityUtils.getSubject();subject.logout();//退出用户return "redirect:/login.jsp";}
}
3.修改退出连接访问退出路径

4.退出之后访问受限资源立即返回认证界面

     Map<String, String> map = new HashMap<String, String>();map.put("/index.jsp","authc");//authc 请求这个index.jsp资源需要认证和授权shiroFilterFactoryBean.setFilterChainDefinitionMap(map);

/index.jsp这种形式要写很多,比较麻烦。通常map.put("/**",“authc”); "/**"表示除了下边配置的login.jsp外所有的都拦截。

     Map<String, String> map = new HashMap<String, String>();map.put("/**","authc");  //authc 请求所以的资源需要认证和授权 包括静态map.put("/user/login","anon");//设置为公共资源,不放行会进入死循环  放行资源放在下面shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
     map.put("/user/login","anon");map.put("/register.jsp","anon");map.put("/user/register","anon");map.put("/**","authc");  //authc 请求这个资源需要认证和授权

6.7 MD5、Salt的认证实现

1.开发数据库注册

0.开发注册界面
<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>

1.创建数据表结构
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (`id` int(6) NOT NULL AUTO_INCREMENT,`username` varchar(40) DEFAULT NULL,`password` varchar(40) DEFAULT NULL,`salt` varchar(255) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;SET FOREIGN_KEY_CHECKS = 1;

2.项目引入依赖
<!--mybatis相关依赖-->
<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.2</version>
</dependency><!--mysql-->
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.38</version>
</dependency><!--druid-->
<dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.1.19</version>
</dependency>
3.配置application.properties配置文件
server.port=8888
server.servlet.context-path=/shiro
spring.application.name=shirospring.mvc.view.prefix=/
spring.mvc.view.suffix=.jsp
#新增配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/shiro?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=rootmybatis.type-aliases-package=com.baizhi.springboot_jsp_shiro.entity
mybatis.mapper-locations=classpath:com/baizhi/mapper/*.xmllogging.level.com.baizhi.dao=debug   打印dao包下的日志

4.创建entity
@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
public class User {private String  id;private String username;private String password;private String salt;
}

@Accessors(chain=true)
链式访问,该注解设置chain=true,生成setter方法返回this(也就是返回的是对象),代替了默认的返回void。

 public static void main(String[] args) {//开起chain=true后可以使用链式的setUser user=new User().setAge(31).setName("pollyduan");//返回对象System.out.println(user);}
5.创建DAO接口
@Mapper
public interface UserDAO {void save(User user);
}
6.开发mapper配置文件
<insert id="save" parameterType="User" useGeneratedKeys="true" keyProperty="id">insert into t_user values(#{id},#{username},#{password},#{salt})
</insert>
7.开发service接口
public interface UserService {//注册用户方法void register(User user);
}
8.创建salt工具类
public class SaltUtils {/*** 生成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();}
}
9.开发service实现类
@Service
@Transactional
public class UserServiceImpl implements UserService {@Autowiredprivate UserDAO userDAO;@Overridepublic void register(User user) {//处理业务调用dao//1.生成随机盐String salt = SaltUtils.getSalt(8);//2.将随机盐保存到数据user.setSalt(salt);//3.明文密码进行md5 + salt + hash散列Md5Hash md5Hash = new Md5Hash(user.getPassword(),salt,1024);user.setPassword(md5Hash.toHex());userDAO.save(user);}
}
10.开发Controller
@Controller
@RequestMapping("user")
public class UserController {@Autowiredprivate UserService userService;/*** 用户注册*/@RequestMapping("register")public String register(User user) {try {userService.register(user);return "redirect:/login.jsp";}catch (Exception e){e.printStackTrace();return "redirect:/register.jsp";}}
}
11.启动项目进行注册


2.开发数据库认证

0.开发DAO
@Mapper
public interface UserDAO {void save(User user);//根据身份信息认证的方法User findByUserName(String username);
}
1.开发mapper配置文件
<select id="findByUserName" parameterType="String" resultType="User">select id,username,password,salt from t_userwhere username = #{username}
</select>
2.开发Service接口
public interface UserService {//注册用户方法void register(User user);//根据用户名查询业务的方法User findByUserName(String username);
}
3.开发Service实现类
@Service("userService")
@Transactional
public class UserServiceImpl implements UserService {@Autowiredprivate UserDAO userDAO;@Overridepublic User findByUserName(String username) {return userDAO.findByUserName(username);}
}
4.开发在工厂中获取bean对象的工具类(当有别的bean要被注入时,由于局限原因不能使用@Component直接注入时可以使用该方法)在这里因为 自定义realm的认证阶段属于filter,当时的spring bean还没有读取进来
@Component
public class ApplicationContextUtils implements ApplicationContextAware {private static ApplicationContext context;@Overridepublic void setApplicationContext(ApplicationContext applicationContext)                                                        throws BeansException {this.context = applicationContext;}//根据bean名字获取工厂中指定bean 对象public static Object getBean(String beanName){return context.getBean(beanName);}
}
5.修改自定义realm
 @Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {System.out.println("==========================");//根据身份信息String principal = (String) token.getPrincipal();//在工厂中获取service对象UserService userService = (UserService)                                                              ApplicationContextUtils.getBean("userService");//根据身份信息查询User user = userService.findByUserName(principal);//!ObjectUtils.isEmpty(user)和user!=null一样if(!ObjectUtils.isEmpty(user)){ //返回数据库信息return new                                                                       SimpleAuthenticationInfo(user.getUsername(),user.getPassword(),                        ByteSource.Util.bytes(user.getSalt()),this.getName());}return null;}
6.修改ShiroConfig中realm使用凭证匹配器以及hash散列
@Bean
public Realm getRealm(){CustomerRealm customerRealm = new CustomerRealm();//设置hashed凭证匹配器HashedCredentialsMatcher credentialsMatcher = new                                                                          HashedCredentialsMatcher();//设置md5加密credentialsMatcher.setHashAlgorithmName("md5");//设置散列次数credentialsMatcher.setHashIterations(1024);customerRealm.setCredentialsMatcher(credentialsMatcher);return customerRealm;
}

6.8 授权实现

0.页面资源授权(jsp页面)
//引入标签
<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %><shiro:hasAnyRoles name="user,admin"><li><a href="">用户管理</a><ul><shiro:hasPermission name="user:add:*">  资源权限<li><a href="">添加</a></li></shiro:hasPermission><shiro:hasPermission name="user:delete:*"><li><a href="">删除</a></li></shiro:hasPermission><shiro:hasPermission name="user:update:*"><li><a href="">修改</a></li></shiro:hasPermission><shiro:hasPermission name="user:find:*"><li><a href="">查询</a></li></shiro:hasPermission></ul></li></shiro:hasAnyRoles>    角色权限<shiro:hasRole name="admin"><li><a href="">商品管理</a></li><li><a href="">订单管理</a></li><li><a href="">物流管理</a></li></shiro:hasRole>
1.代码方式授权
@RequestMapping("save")
public String save(){System.out.println("进入方法");//获取主体对象Subject subject = SecurityUtils.getSubject();//代码方式if (subject.hasRole("admin")) {System.out.println("保存订单!");}else{System.out.println("无权访问!");}//基于权限字符串//....return "redirect:/index.jsp";
}

2.方法调用授权
  • @RequiresRoles 用来基于角色进行授权
  • @RequiresPermissions 用来基于权限进行授权
@RequestMapping("order")
public class OrderController(){@RequiresRoles(value={"admin","user"})//用来判断角色  同时具有 admin userc@RequiresPermissions("user:update:01") //用来判断权限字符串@RequestMapping("save")public String save(){System.out.println("进入方法");return "redirect:/index.jsp";}
}


3.授权数据持久化

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;-- ----------------------------
-- Table structure for t_pers
-- ----------------------------
DROP TABLE IF EXISTS `t_pers`;
CREATE TABLE `t_pers` (`id` int(6) NOT NULL AUTO_INCREMENT,`name` varchar(80) DEFAULT NULL,`url` varchar(255) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ----------------------------
-- Table structure for t_role
-- ----------------------------
DROP TABLE IF EXISTS `t_role`;
CREATE TABLE `t_role` (`id` int(6) NOT NULL AUTO_INCREMENT,`name` varchar(60) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ----------------------------
-- Table structure for t_role_perms
-- ----------------------------
DROP TABLE IF EXISTS `t_role_perms`;
CREATE TABLE `t_role_perms` (`id` int(6) NOT NULL,`roleid` int(6) DEFAULT NULL,`permsid` int(6) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ----------------------------
-- Table structure for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (`id` int(6) NOT NULL AUTO_INCREMENT,`username` varchar(40) DEFAULT NULL,`password` varchar(40) DEFAULT NULL,`salt` varchar(255) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;-- ----------------------------
-- Table structure for t_user_role
-- ----------------------------
DROP TABLE IF EXISTS `t_user_role`;
CREATE TABLE `t_user_role` (`id` int(6) NOT NULL,`userid` int(6) DEFAULT NULL,`roleid` int(6) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;SET FOREIGN_KEY_CHECKS = 1;



entity(实体)类:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class User {private String id;private String username;private String password;private String salt;private List<Role> roles;//定义角色集合
}@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class Role {private String id;private String name;
}public class Perms {private String id;private String name;private String url;
}
4.创建dao方法
 //根据用户名查询所有角色
User findRolesByUserName(String username);
//根据角色id查询权限集合
List<Perms> findPermsByRoleId(String id);
5.mapper实现

<resultMap id="userMap" type="User"><id column="uid" property="id"/><result column="username" property="username"/><!--角色信息-->                             //里边装的是什么类型<collection property="roles" javaType="list" ofType="Role">  // ***********<id column="id" property="id"/><result column="rname" property="name"/></collection>
</resultMap><select id="findRolesByUserName" parameterType="String" resultMap="userMap">SELECT u.id uid,u.username,r.id,r.NAME rnameFROM t_user uLEFT JOIN t_user_role urON u.id=ur.useridLEFT JOIN t_role rON ur.roleid=r.idWHERE u.username=#{username}
</select><select id="findPermsByRoleId" parameterType="String" resultType="Perms">SELECT p.id,p.NAME,p.url,r.NAMEFROM t_role rLEFT JOIN t_role_perms rpON r.id=rp.roleidLEFT JOIN t_perms p ON rp.permsid=p.idWHERE r.id=#{id}
</select>
6.Service接口
//根据用户名查询所有角色
User findRolesByUserName(String username);
//根据角色id查询权限集合
List<Perms> findPermsByRoleId(String id);
7.Service实现
@Override
public List<Perms> findPermsByRoleId(String id) {return userDAO.findPermsByRoleId(id);
}@Override
public User findRolesByUserName(String username) {return userDAO.findRolesByUserName(username);
}
8.修改自定义realm

CollectionUtils.isEmpty(user.getRoles())角色是否为空

public class CustomerRealm extends AuthorizingRealm {@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {//获取身份信息String primaryPrincipal = (String) principals.getPrimaryPrincipal();//根据主身份信息获取角色 和 权限信息UserService userService = (UserService)                                                            ApplicationContextUtils.getBean("userService");User user = userService.findRolesByUserName(primaryPrincipal);//授权角色信息  !CollectionUtils.isEmpty(user.getRoles())if(!CollectionUtils.isEmpty(user.getRoles())){SimpleAuthorizationInfo simpleAuthorizationInfo = new                                                                SimpleAuthorizationInfo();user.getRoles().forEach(role->{simpleAuthorizationInfo.addRole(role.getName());//到此授权角色信息结束,可以只做角色信息,看要求//权限信息List<Perms> perms =                                                                           userService.findPermsByRoleId(role.getId());if(!CollectionUtils.isEmpty(perms)){perms.forEach(perm->{simpleAuthorizationInfo.addStringPermission(perm.getName());});}});return simpleAuthorizationInfo;}return null;}
}

9.启动测试


6.9 使用CacheManager

页面中有权限的数据,每次访问,每次刷新都会重新访问一次数据库;一个用户的权限数据基本不会经常发生变化。每一次刷新都会去调用doGetAuthorizationInfo()去数据库获取信息。

1.Cache 作用

  • Cache 缓存: 计算机内存中一段数据
  • 作用: 用来减轻DB的访问压力,从而提高系统的查询效率
  • 流程:

在内存中存放,每次获取会很快。查询多增删少

2.使用shiro中默认EhCache实现缓存

EhCache: 是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate中默认的CacheProvider(缓存产品)。

公司中大多数用redis做缓存。

1.引入依赖
<!--引入shiro和ehcache-->
<dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-ehcache</artifactId><version>1.5.3</version>
</dependency>
2.开启缓存
//3.创建自定义realm@Beanpublic Realm getRealm(){CustomerRealm customerRealm = new CustomerRealm();//修改凭证校验匹配器HashedCredentialsMatcher credentialsMatcher = new                                                                         HashedCredentialsMatcher();//设置加密算法为md5credentialsMatcher.setHashAlgorithmName("MD5");//设置散列次数credentialsMatcher.setHashIterations(1024);customerRealm.setCredentialsMatcher(credentialsMatcher);//开启缓存管理器customerRealm.setCacheManager(new EhCacheManager());customerRealm.setCachingEnabled(true);开启全局缓存customerRealm.setAuthenticationCachingEnabled(true);//开启认证缓存customerRealm.setAuthenticationCacheName("authenticationCache");//可以指定名字,也可以不指定,有默认名字customerRealm.setAuthorizationCachingEnabled(true);//开启授权缓存customerRealm.setAuthorizationCacheName("authenorizationCache");return customerRealm;}

只有第一次会去数据库查找,之后在缓存中找。

在自己应用的一块内存里边,关掉程序就没有了,还会去查数据库,实现的是本地缓存,就是应用内部的缓存。可以结合redis做成分布式缓存,把缓存放在redis中,与当前应用是否宕机、停止,没有关系,只要redis中有缓存,每次都去缓存中找,不去数据库。

3.启动刷新页面进行测试
  • 注意:如果控制台没有任何sql展示说明缓存已经开启

3.shiro中使用Redis作为缓存实现

1.引入redis依赖
<!--redis整合springboot-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.配置redis连接
spring.redis.port=6379
spring.redis.host=localhost
spring.redis.database=0

3.启动redis服务
➜  bin ls
dump.rdb        redis-check-aof redis-cli       redis-server    redis.conf
redis-benchmark redis-check-rdb redis-sentinel  redis-trib.rb
➜  bin ./redis-server redis.conf

4.开发RedisCacheManager
//自定义shiro缓存管理器
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); //也可以不传参数}
}
5.开发RedisCache实现
public class RedisCache<K,V> implements Cache<K,V> {   @Overridepublic V get(K k) throws CacheException {System.out.println("获取缓存:"+ k); return (V)                                                                        getRedisTemplate().opsForValue().get(k.toString());}@Overridepublic V put(K k, V v) throws CacheException {System.out.println("设置缓存key: "+k+" value:"+v);getRedisTemplate().opsForValue().set(k.toString(),v);return null;}......
}
//以上这些是使用无参构造,没有管cacheName,RedisCacheManager类中方
//法返回return new RedisCache<K,V>();无参

如果想要把缓存名字也考虑进去,用下面这种。用Hash模型,getRedisTemplate().opsForHash()…

map里边还有map,相当于Map<String,Map<String,Object>,大key需要序列化,内部key也要序列化。

public class RedisCache<K,V> implements Cache<K,V> {private String cacheName//有参构造有、public RedisCache() {}public RedisCache(String cacheName) {this.cacheName = cacheName;}@Overridepublic V get(K k) throws CacheException {System.out.println("获取缓存:"+ k); return (V)                                                                          getRedisTemplate().opsForHash().get(this.cacheName,k.toString());}@Overridepublic 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;}//点击退出用户,调用的是remove()不是clear()   只有一个用户访问时,点击退出,下一次会去数据库查找,缓存中没有了,因为只有一条用户的数据,map中没有数据时,就会把整个map清空掉。@Overridepublic v remove(k k) throws CacheException {return                                                                       (v) getRedisTemplate().opsForHash().delete(this.cacheName,k.toString());}@Overridepublic void clear() throws CacheException {getRedisTemplate().delete(this.cacheName);}@Overridepublic int size() {return                                                                          getRedisTemplate().opsForHash().size(this.cacheName).intValue();//前边返回long类型,intValue()使其返回int类型}@Overridepublic Set<k> keys() {return getRedisTemplate().opsForHash().keys(this.cacheName);}@Overridepublic Collection<v> values() {return getRedisTemplate().opsForHash().values(this.cacheName);}//封装获取redisTemplate//简化书写 上边都需要redisTemplate 做一个简单的封装private RedisTemplate getRedisTemplate(){RedisTemplate redisTemplate = (RedisTemplate)                                                     ApplicationContextUtils.getBean("redisTemplate");redisTemplate.setKeySerializer(new StringRedisSerializer());大key序列化redisTemplate.setHashKeySerializer(new StringRedisSerializer());//内部key序列化return redisTemplate;}
}

new RedisCacheManager()

@Configuration
public class ShiroConfig {@Beanpublic Realm getRealm(){CustomerRealm realm = new CustomerRealm();HashedCredentialsMatcher credentialsMatcher = new                                                                        HashedCredentialsMatcher();credentialsMatcher.setHashAlgorithmName("md5");credentialsMatcher.setHashIterations(1024);realm.setCredentialsMatcher(credentialsMatcher);realm.setCacheManager(new RedisCacheManager()); //*************realm.setCachingEnabled(true);realm.setAuthenticationCachingEnabled(true);realm.setAuthorizationCachingEnabled(true);return realm;}
}

redis默认使用对象序列化策略,所有存入对象后get看到的是一串字符。

6.启动项目测试发现报错

  • 错误解释: 由于shiro中提供的simpleByteSource实现没有实现序列化,所以在认证时出现错误信息

  • 解决方案: 需要自动salt实现序列化

    • 自定义salt实现序列化

      //自定义salt实现  实现序列化接口
      public class MyByteSource extends SimpleByteSource implements Serializable {public MyByteSource(String string) {super(string);}
      }
    • 在realm中使用自定义salt; new MyByteSource(user.getSalt())

       @Override
      protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {System.out.println("==========================");//根据身份信息String principal = (String) token.getPrincipal();//在工厂中获取service对象UserService userService = (UserService)                                                       ApplicationContextUtils.getBean("userService");User user = userService.findByUserName(principal);if(!ObjectUtils.isEmpty(user)){return new                            SimpleAuthenticationInfo(user.getUsername(),user.getPassword(), new MyByteSource(user.getSalt()),this.getName());}return null;
      }

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SUEMGZ8F-1631715710787)(Shiro 实战教程.assets/image-20200530101301543.png)]

7.再次启动测试,发现可以成功放入redis缓存(下面就是序列化后的对象)

第一行记录授权信息

第二行记录认证信息

要使用hash命令进行操作 hgetall

hgetall  com.baizhi.shiro.realm.CustomerRealm.authenticationCache

4. 加入验证码验证

把之前的验证码工具类VeriffyCodeUtils拿来用,放到utils中。

0.开发页面加入验证码
  • 开发控制器

    @RequestMapping("getImage")
    public void getImage(HttpSession session, HttpServletResponse response) throws IOException {//生成验证码String code = VerifyCodeUtils.generateVerifyCode(4);//验证码放入sessionsession.setAttribute("code",code);//之后要取出验证//验证码存入图片ServletOutputStream os = response.getOutputStream();response.setContentType("image/png");//响应流类型VerifyCodeUtils.outputImage(220,60,os,code);
    }
  • 放行验证码

  • 开发页面

    <form action="${pageContext.request.contextPath}/user/login"                                                                         method="post">用户名:<input type="text" name="username"><br>密&nbsp;码:<input type="text" name="password"><br>请输入验证码:<input type="text" name="code"><img src="${pageContext.request.contextPath}/user/getImage" alt=""><br><input type="submit" value="登录">
    </form>

  • 修改认证流程

    @RequestMapping("login")public String login(String username, String password,String code,HttpSession session) {//比较验证码String codes = (String) session.getAttribute("code");try {if (codes.equalsIgnoreCase(code)){//获取主体对象Subject subject = SecurityUtils.getSubject();subject.login(newUsernamePasswordToken(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();System.out.println(e.getMessage());}return "redirect:/login.jsp";}

    访问出现错误:

    ​ (no valid constructor 没有有效的构造函数)

    原因MyByteSource没有无参构造,Redis在反序列化时要用到无参构造。

    用下面的办法解决:implements ByteSource,Serializable;然后把SimpleByteSource中全部内容拿出来改造,把里边SimpleByteSource换成MyByteSource,在加入一个无参构造;这个无参构造仅仅用来做序列化。

  • 修改salt不能序列化的问题

    //之前继承MyByteSource extends SimpleByteSource。SimpleByteSource中没有无参构造,无参构造Redis反序列化时要使用,所以自己需要在实现一个。
    //*父类没有无参构造,子类也不能有无参构造,因为子类第一行默认调用父类无参构造super()
    //自定义salt实现  实现序列化接口  把SimpleByteSource中全部内容拿出来改造
    public class MyByteSource implements ByteSource,Serializable {//加入无参数构造方法实现序列化和反序列化public MyByteSource(){}private  byte[] bytes;private String cachedHex;private String cachedBase64;public MyByteSource(byte[] bytes) {this.bytes = bytes;}public MyByteSource(char[] chars) {this.bytes = CodecSupport.toBytes(chars);}public MyByteSource(String string) {this.bytes = CodecSupport.toBytes(string);}public MyByteSource(ByteSource source) {this.bytes = source.getBytes();}public MyByteSource(File file) {this.bytes = (new MyByteSource.BytesHelper()).getBytes(file);}public MyByteSource(InputStream stream) {this.bytes = (new MyByteSource.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);}}
    }

页面展示用户身份信息(jsp中):

<shiro:principal/>  拿到用户主身份信息
<shiro:authenticated>认证之后展示的内容
</shiro:authenticated><shiro:noAuthenticated>没有认证展示的内容
</shiro:noAuthenticated>

7.Shiro整合springboot之thymeleaf权限控制

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
spring.thymeleaf.cache=false
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.resources.static-locations=classpath:/static/ #调整静态资源位置,默认也是这里,可以不写,当想要改变位置时配置
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
</head>
<body>
<h1>用户登录</h1>
<form th:action="@{/user/login}" method="post">用户名:<input type="text" name="username"><br>&nbsp;码:<input type="text" name="password"><br>请输入验证码:<input type="text" name="code"><img th:src="@{/user/getImage}"><br><input type="submit" value="登录">
</form>
</body>
</html>

上边这些是常规的thymeleaf操作。

1.引入扩展依赖

<dependency><groupId>com.github.theborakompanioni</groupId><artifactId>thymeleaf-extras-shiro</artifactId><version>2.0.0</version>
</dependency>

2.页面中引入命名空间

  • xmlns:shiro=“http://www.pollix.at/thymeleaf/shiro”
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
......

3.常见权限控制标签使用

<!-- 验证当前用户是否为“访客”,即未认证(包含未记住)的用户。 -->
<p shiro:guest="">Please <a href="login.html">login</a></p><!-- 认证通过或已记住的用户。 -->
<p shiro:user="">Welcome back John! Not John? Click <a href="login.html">here</a> to login.
</p><!-- 已认证通过的用户。不包含已记住的用户,这是与user标签的区别所在。 -->
<p shiro:authenticated="">Hello, <span shiro:principal=""></span>, how are you today?
</p>
<a shiro:authenticated="" href="updateAccount.html">Update your contact information</a><!-- 输出当前用户信息,通常为登录帐号信息。 -->
<p>Hello, <shiro:principal/>, how are you today?</p><!-- 未认证通过用户,与authenticated标签相对应。与guest标签的区别是,该标签包含已记住用户。 -->
<p shiro:notAuthenticated="">Please <a href="login.html">login</a> in order to update your credit card information.
</p><!-- 验证当前用户是否属于该角色。 -->
<a shiro:hasRole="admin" href="admin.html">Administer the system</a><!-- 拥有该角色 --><!-- 与hasRole标签逻辑相反,当用户不属于该角色时验证通过。 -->
<p shiro:lacksRole="developer"><!-- 没有该角色 -->Sorry, you are not allowed to developer the system.
</p><!-- 验证当前用户是否属于以下所有角色。 -->
<p shiro:hasAllRoles="developer, 2"><!-- 角色与判断 -->You are a developer and a admin.
</p><!-- 验证当前用户是否属于以下任意一个角色。  -->
<p shiro:hasAnyRoles="admin, vip, developer,1"><!-- 角色或判断 -->You are a admin, vip, or developer.
</p><!--验证当前用户是否拥有指定权限。  -->
<a shiro:hasPermission="userInfo:add" href="createUser.html">添加用户</a><!-- 拥有权限 --><!-- 与hasPermission标签逻辑相反,当前用户没有制定权限时,验证通过。 -->
<p shiro:lacksPermission="userInfo:del"><!-- 没有权限 -->Sorry, you are not allowed to delete user accounts.
</p><!-- 验证当前用户是否拥有以下所有角色。 -->
<p shiro:hasAllPermissions="userInfo:view, userInfo:add"><!-- 权限与判断 -->You can see or add users.
</p><!-- 验证当前用户是否拥有以下任意一个权限。  -->
<p shiro:hasAnyPermissions="userInfo:view, userInfo:del"><!-- 权限或判断 -->You can see or delete users.
</p>
<a shiro:hasPermission="pp" href="createUser.html">Create a new User</a>

4.加入shiro的方言配置

  • 页面标签不起作用一定要记住加入方言处理
//在ShiroConfig配置文件中
@Bean(name = "shiroDialect")
public ShiroDialect shiroDialect(){return new ShiroDialect();
}

Shiro 实战教程相关推荐

  1. Shiro 实战教程(全)

    目录 1.权限的管理 1.1 什么是权限管理 1.2 什么是身份认证 1.3 什么是授权 2.什么是shiro 3.shiro的核心架构 3.1 Subject 3.2 SecurityManager ...

  2. 一文总结 Shiro 实战教程

    ✅作者简介:2022年博客新星 第八.热爱国学的Java后端开发者,修心和技术同步精进.

  3. Spring Boot整合Shiro + JSP教程(用户认证,权限管理,图片验证码)

    在此首先感谢**编程不良人**up主提供的视频教程 代码都是跟着up的视频敲的,遇到的一些问题也是通过CSDN博主提供的教程解决的,在此也感谢那些提供bug解决方案的前辈们~ 项目完整代码已经发布到g ...

  4. 毕业设计-基于SSM框架大学教务管理平台项目开发实战教程(附源码)

    文章目录 1.项目简介 2.项目收获 3.项目技术栈 4.测试账号 5.项目部分截图 6.常见问题 毕业设计-基于SSM框架大学教务管理平台项目实战教程-附源码 课程源码下载地址:https://do ...

  5. PyTorch 高级实战教程:基于 BI-LSTM CRF 实现命名实体识别和中文分词

    20210607 https://blog.csdn.net/u011828281/article/details/81171066 前言:译者实测 PyTorch 代码非常简洁易懂,只需要将中文分词 ...

  6. ArcGIS水文分析实战教程(9)雨量计算与流量统计

    ArcGIS水文分析实战教程(9)雨量计算与流量统计 本章导读:降水是水文循环中重要的一环,降水包括雨.雪.雾.露.雹等,本章介绍的是降雨的环节.通过雨量站与插值的方式,实现雨量的空间分布就算,为水文 ...

  7. 宏基因组分析实战教程1. 背景知识

    上次我写的学习经验和推荐的教程--<微生物组入门必读+宏基因组实操课程=新老司机赶快上车>,小伙伴们当天阅读破2700+人次,3.5天破3000+,达到了宏基因组快车满三千人发车的要求.我 ...

  8. js模板字符串自定义类名_【Vue.js 入门到实战教程】07Vue 组件注册 | 基本使用和组件嵌套...

    来源 | https://xueyuanjun.com/post/21929除了前面介绍的基本语法之外,Vue.js 还支持通过组件构建复杂的功能模块,组件可以称得上是 Vue.js 的灵魂,是 Vu ...

  9. python商业爬虫教程_廖雪峰老师的Python商业爬虫课程 Python网络爬虫实战教程 体会不一样的Python爬虫课程...

    廖雪峰老师的Python商业爬虫课程 Python网络爬虫实战教程 体会不一样的Python爬虫课程 1.JPG (53.51 KB, 下载次数: 1) 2019-8-9 08:15 上传 2.JPG ...

最新文章

  1. 商汤再挖MSRA大将:R-FCN作者代季峰加盟任执行研究总监
  2. Redis作者的公开信:开源维护者的挣扎和无奈
  3. gradle 的cas 转maven_Gradle 历险记(二):从 maven 到 gradle 迁移的一些坑
  4. SpringMVC-HandlerInterceptor拦截器的使用与参数详解
  5. 【毕业设计】java银行帐目管理系统(源代码+论文)
  6. 自反馈与 Mandelbrot set
  7. ssh配置config文件,实现vscode免密登陆
  8. html拷贝图片不显示,【原创软件小知识】——拷贝cad文件中光栅图片不显示的问题...
  9. python 加汉明窗_什么是汉明窗?加Hanmming窗的作用?
  10. 77页智慧应急解决方案 2022
  11. mysql删除某天前的数据
  12. 怎么从转移特性曲线上看dibl_MOS管工作原理详解:各种mos管的转移特性曲线分析...
  13. 我希望进入大学时就能知道的一些事儿 -----作者:瞬息之间
  14. SAP中利用标准成本报表计算成品人工成本及组成实例
  15. 289714-02-9,Biotin-PEG3-alcohol,Biotin-PEG3-OH含有一个生物素基团和一个与多种官能团反应的末端伯羟基
  16. BZOJ1016 || 洛谷P4208 [JSOI2008]最小生成树计数【矩阵树定理】
  17. Security:如何安装 Elastic SIEM 和 EDR
  18. dbms_xplan
  19. JavaScript动态显示鼠标移动轨迹
  20. 计算机校本培训心得,2016信息技术校本研修心得体会

热门文章

  1. 关于细分到字段的权限系统_操作系统中的细分
  2. python中locked_Python锁类| 带示例的locked()方法
  3. 为什么在Python中使用string.join(list)而不是list.join(string)?
  4. php 数据库编码,php怎么设置数据库编码方式
  5. css圆在中心根据宽度缩放_根据CSS中的容器宽度重新缩放字体
  6. 不错!SpringBoot发布Jar包优化瘦身指南!
  7. List 集合去重的 3 种方法
  8. 《大厂内部资料》Redis 性能优化的 13 条军规!全网首发
  9. Sql Server内置函数实现MD5加密
  10. java process exe.exec 执行exe程序