一篇看懂shiro安全框架
本篇框架使用:SpringBoot SpringMvc MyBatis shiro thymeleaf
我们先来了解一下shiro的基本知识
1.什么是Shiro权限框架
Apache Shiro是一个强大且易用的Java安全框架,执行身份认证、授权、加密和会话管理。使用Shiro的易于理解的API,可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
2.Shiro可以做哪些事情
●验证用户身份
●用户访问控制,比如用户是否被赋予了某个角色;是否允许访问某些资源
●在任何环境都可以使用SessionAPI,即使不是WEB项目或没有EJB容器
●事件响应(在身份验证,访问控制期间,或是session生命周期中)
●集成多种用户信息数据源
●SSO- 单点登陆
●Remember Me,记住我
●Shiro尝试在任何应用环境下实现这些功能,而不依赖其他框架、容器或应用服务器。
3.Shiro与Springsecurity区别
1.首先Shiro较之Spring Security, Shiro 在保持强大功能的同时,还在简单性和灵活性方面拥有巨大优势。
2.Shiro是一个强大而灵活的开源安全框架,能够非常清晰的处理认证、授权、加密、会话管理等功能。
3.密码加密
4.框架体系
Shiro 的整体框架大致如下图所示(图片来自互联网):
Authentication(认证), Authorization(授权), Session Management(会话管理), Cryptography(加密)代表Shiro应用安全的四大基石。
它们分别是:
Authentication(认证):用户身份识别,通常被称为用户“登录”。
Authorization(授权):访问控制。比如某个用户是否具有某个操作的使用权限。
Session Management(会话管理):特定于用户的会话管理,甚至在非web 应用程序。
Cryptography(加密):在对数据源使用加密算法加密的同时,保证易于使用。
除此之外,还有其他的功能来支持和加强这些不同应用环境下安全领域的关注点。
特别是对以下的功能支持:
Web支持:Shiro 提供的 web 支持 api ,可以很轻松的保护 web 应用程序的安全。
缓存:缓存是 Apache Shiro 保证安全操作快速、高效的重要手段。
并发:Apache Shiro 支持多线程应用程序的并发特性。
测试:支持单元测试和集成测试,确保代码和预想的一样安全。
“Run As”:这个功能允许用户在许可的前提下假设另一个用户的身份。
“Remember Me”:跨 session 记录用户的身份,只有在强制需要时才需要登录。
5.shiro主要流程
在概念层,Shiro 架构包含三个主要的理念:Subject, SecurityManager 和 Realm。下面的图展示了这些组件如何相互作用,我们将在下面依次对其进行描述。
1.Subject:当前用户,Subject 可以是一个人,但也可以是第三方服务、守护进程帐户、时钟守护任务或者其它-当前和软件交互的任何事件。
2. SecurityManager:管理所有Subject,SecurityManager 是Shiro 架构的核心,配合内部安全组件共同组成安全伞。
3.Realms:用于进行权限信息的验证,我们自己实现。Realm本质上是一个特定的安全DAO:它封装与数据源连接的细节,得到Shiro所需的相关的数据。在配置Shiro的时候,你必须指定至少一个Realm来实现认证(authentication)和/或授权( authorization)。
了解完shiro的基础知识,接下来我们看代码的实现
数据库表
user表
role表
代码目录结构
pom.xml(加入shiro依赖包)
<dependencies> <!--包含spirng Mvc ,tomcat的包--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.10</version> <scope>provided</scope> </dependency> <!-- 包含spirng Mvc ,tomcat的包包含requestMapping restController 等注解 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- mybatis 与 spring boot 2.x的整合包 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.1</version> </dependency> <!-- 连接mysql数据源 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.17</version> </dependency> <!-- mysql包 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> </dependencies>
配置类application.properties(配置连接mysql数据库,mybatis,thymeleaf配置)
server.port=8080#数据库连接spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTCspring.datasource.driver-class-name=com.mysql.jdbc.Driverspring.datasource.username=rootspring.datasource.password=123456spring.datasource.type=com.alibaba.druid.pool.DruidDataSource#mybatis配置mybatis.type-aliases-package=com.demo.shiro.entitymybatis.mapper-locations=classpath:mapper/*.xml# thymeleaf页面模板配置spring.thymeleaf.prefix= classpath:/templates/spring.thymeleaf.suffix=.ftl
项目启动类ShiroApplication(对mapper进行扫包)
package com.demo.shiro;import org.mybatis.spring.annotation.MapperScan;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication@MapperScan(basePackages = "com.demo.shiro.mapper")public class ShiroApplication { public static void main(String[] args) { SpringApplication.run(ShiroApplication.class, args); } }
实体类RoleMapper
/******************************************************************************* * Package: com.demo.shiro.entity * Type: RoleEntity * Date: 2020/12/5 11:06 * * Copyright (c) 2020 HUANENG GUICHENG TRUST CORP.,LTD All Rights Reserved. * * You may not use this file except in compliance with the License. *******************************************************************************/package com.demo.shiro.entity;import lombok.Data;import lombok.Getter;import lombok.Setter;/** * 角色实体 * * @author Yujiaqi * @date 2020/12/5 11:06 */@Data@Getter@Setterpublic class RoleEntity { private Integer id; private String role; private String permissionurl; public RoleEntity(){}; public RoleEntity(Integer id, String role, String permissionurl) { this.id = id; this.role = role; this.permissionurl = permissionurl; }}
实体类UserMapper
/******************************************************************************* * Package: com.demo.shiro.entity * Type: UserEntity * Date: 2020/12/5 11:06 * * Copyright (c) 2020 HUANENG GUICHENG TRUST CORP.,LTD All Rights Reserved. * * You may not use this file except in compliance with the License. *******************************************************************************/package com.demo.shiro.entity;import jdk.nashorn.internal.objects.annotations.Constructor;import lombok.Data;import lombok.Getter;import lombok.Setter;/** * 用户实体 * * @author Yujiaqi * @date 2020/12/5 11:06 */@Data@Getter@Setterpublic class UserEntity { private int id; private String userName; private String realName; private String passWord; private String role; public UserEntity(){} public UserEntity(int id, String userName, String realName, String passWord, String role) { this.id = id; this.userName = userName; this.realName = realName; this.passWord = passWord; this.role = role; }}
RoleMapper类
/******************************************************************************* * Package: com.demo.shiro.mapper * Type: RoleMapper * Date: 2020/12/5 11:06 * * Copyright (c) 2020 HUANENG GUICHENG TRUST CORP.,LTD All Rights Reserved. * * You may not use this file except in compliance with the License. *******************************************************************************/package com.demo.shiro.mapper;import com.demo.shiro.entity.RoleEntity;import org.springframework.stereotype.Repository;import java.util.List;/** * TODO your comment * * @author Yujiaqi * @date 2020/12/5 11:06 */@Repositorypublic interface RoleMapper { /** * 查询所有角色表信息 * @return */ List<RoleEntity> selectRoleAll();}
UserMapper类
/******************************************************************************* * Package: com.demo.shiro.mapper * Type: UserMapper * Date: 2020/12/5 11:06 * * Copyright (c) 2020 HUANENG GUICHENG TRUST CORP.,LTD All Rights Reserved. * * You may not use this file except in compliance with the License. *******************************************************************************/package com.demo.shiro.mapper;import com.demo.shiro.entity.UserEntity;import org.apache.ibatis.annotations.Param;/** * 根据用户名查询数据 * * @author Yujiaqi * @date 2020/12/5 11:06 */public interface UserMapper { UserEntity findByUsername(@Param(value = "userName") String userName);}
RoleMapper.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.demo.shiro.mapper.RoleMapper"> <select id="selectRoleAll" resultType="com.demo.shiro.entity.RoleEntity"> SELECT * FROM test.ROLE </select></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.demo.shiro.mapper.UserMapper"> <select id="findByUsername" resultType="com.demo.shiro.entity.UserEntity"> SELECT u.id, u.user_name AS userName, u.real_name AS realName, u.PASSWORD, u.role FROM test.USER u WHERE u.user_name = #{userName} </select></mapper>
ShiroConfig配置类(项目启动时加载,配置页面登录拦截,登录权限)
/******************************************************************************* * Package: com.demo.shiro.config * Type: ShiroConfig * Date: 2020/12/5 10:44 * * Copyright (c) 2020 HUANENG GUICHENG TRUST CORP.,LTD All Rights Reserved. * * You may not use this file except in compliance with the License. *******************************************************************************/package com.demo.shiro.config;import com.demo.shiro.entity.RoleEntity;import com.demo.shiro.mapper.RoleMapper;import org.apache.shiro.mgt.SecurityManager;import org.apache.shiro.spring.web.ShiroFilterFactoryBean;import org.apache.shiro.web.mgt.DefaultWebSecurityManager;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import java.util.HashMap;import java.util.List;/** * ShiroConfig * * @author Yujiaqi * @date 2020/12/5 10:44 */@Configurationpublic class ShiroConfig { @Autowired private MayiktRealm mayiktRealm; @Autowired private RoleMapper roleMapper; /** * @Configuration实际上就是@Component,将类注入到spring容器中 */ /** * 配置shiroFilter * @param securityManager * @return */ @Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){ ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); //设定登录页面 shiroFilterFactoryBean.setLoginUrl("/login"); shiroFilterFactoryBean.setSuccessUrl("/home"); HashMap<String, String> map = new HashMap<>(); //不需要登录,就可以访问接口 map.put("/mayiktOpenApi","anon"); //需要要登录才可以访问该接口,采用弹框的形式(authcBasic),表单模式(authc) //要跳转到home页面的用户必须经过登录 map.put("/home","authc");// //设定order接口只能order角色访问// map.put("/order/*","roles[order]");// //设定member接口只能member角色访问// map.put("/member/*","roles[member]"); //通过数据库设计接口访问权限 List<RoleEntity> roleEntities = roleMapper.selectRoleAll(); roleEntities.forEach(t -> { map.put(t.getPermissionurl(),"roles[" t.getRole() "]"); }); shiroFilterFactoryBean.setFilterChainDefinitionMap(map); shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized"); shiroFilterFactoryBean.setSecurityManager(securityManager); return shiroFilterFactoryBean; } @Bean public SecurityManager securityManager(){ //将自定义Realm加进来 DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(mayiktRealm); return securityManager; }}
MayiktRealm类 需要继承AuthorizingRealm类
/******************************************************************************* * Package: com.demo.shiro.config * Type: MayiktRealm * Date: 2020/12/5 10:59 * * Copyright (c) 2020 HUANENG GUICHENG TRUST CORP.,LTD All Rights Reserved. * * You may not use this file except in compliance with the License. *******************************************************************************/package com.demo.shiro.config;import com.alibaba.druid.util.StringUtils;import com.demo.shiro.entity.UserEntity;import com.demo.shiro.mapper.UserMapper;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.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import java.util.Arrays;import java.util.HashSet;/** * 配置拦截器 * * @author Yujiaqi * @date 2020/12/5 10:59 */@Componentpublic class MayiktRealm extends AuthorizingRealm { @Autowired private UserMapper userMapper; /** * 为当前登录成功的用户授予权限和分配角色。 * 在用户登录成功后,每次请求后台接口都需要先访问此方法,进行权限的验证 * @param principal * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) { String username = (String)principal.getPrimaryPrincipal(); UserEntity byUsername = userMapper.findByUsername(username); if (byUsername == null){ return null; } String role = byUsername.getRole(); if (StringUtils.isEmpty(role)){ return null; } SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); //设置权限 HashSet<String> roles = new HashSet<>(); String[] split = role.split(","); Arrays.stream(split).forEach(t ->{ roles.add(t); }); //设置我们的权限 simpleAuthorizationInfo.setRoles(roles); return simpleAuthorizationInfo; } /** * 用来验证当前登录的用户,获取认证信息。 * @param authenticationToken * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { String userName = (String) authenticationToken.getPrincipal(); if (userName == null){ return null; } UserEntity byUsername = userMapper.findByUsername(userName); if(byUsername == null){ return null; } //使用数据库db的密码验证用户输入的密码 //这里传入的password(这里是从数据库获取的)和token(filter中登录时生成的)中的password做对比,如果相同就允许登录,不相同就抛出异常。 SimpleAuthenticationInfo simpleAuthorizationInfo = new SimpleAuthenticationInfo(userName, byUsername.getPassWord(), byUsername.getRealName()); return simpleAuthorizationInfo; }}
MD5Util类(进行密码加密)
/******************************************************************************* * Package: com.demo.shiro.config * Type: MD5Util * Date: 2020/12/5 11:05 * * Copyright (c) 2020 HUANENG GUICHENG TRUST CORP.,LTD All Rights Reserved. * * You may not use this file except in compliance with the License. *******************************************************************************/package com.demo.shiro.config;import java.security.MessageDigest;/** * 用于密码加密 * * @author Yujiaqi * @date 2020/12/5 11:05 */public class MD5Util { private static final String SALT = "yujiaqi"; public static String encode(String password) { password = password SALT; MessageDigest md5 = null; try { md5 = MessageDigest.getInstance("MD5"); } catch (Exception e) { throw new RuntimeException(e); } char[] charArray = password.toCharArray(); byte[] byteArray = new byte[charArray.length]; for (int i = 0; i < charArray.length; i ) byteArray[i] = (byte) charArray[i]; byte[] md5Bytes = md5. digest( byteArray); StringBuffer hexValue = new StringBuffer(); for (int i = 0; i < md5Bytes. length; i ) { int val = ((int) md5Bytes[i]) & 0xff; if(val<16){ hexValue . append("0"); } hexValue.append(Integer. toHexString(val)); } return hexValue. toString(); } public static void main(String[] args) { System.out.println(encode("123456")); //密码只能加密,不能解密,只能通过暴力破解 }}
IndexController登录入口类
/******************************************************************************* * Package: com.demo.shiro.controller * Type: IndexController * Date: 2020/12/5 10:49 * * Copyright (c) 2020 HUANENG GUICHENG TRUST CORP.,LTD All Rights Reserved. * * You may not use this file except in compliance with the License. *******************************************************************************/package com.demo.shiro.controller;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;/** * IndexController * * @author Yujiaqi * @date 2020/12/5 10:49 */@Controllerpublic class IndexController { @RequestMapping("/home") public String home(){ return "home"; } @ResponseBody @RequestMapping("/mayiktOpenApi") public String mayikt(){ return "不需要任何登录和权限都可以访问到"; } @ResponseBody @RequestMapping("/unauthorized") public String unauthorized(){ return "你的角色暂时没有该权限,无法访问该接口"; } }
LoginController类
/******************************************************************************* * Package: com.demo.shiro.controller * Type: LoginController * Date: 2020/12/5 13:54 * * Copyright (c) 2020 HUANENG GUICHENG TRUST CORP.,LTD All Rights Reserved. * * You may not use this file except in compliance with the License. *******************************************************************************/package com.demo.shiro.controller;import com.demo.shiro.config.MD5Util;import com.demo.shiro.entity.UserEntity;import org.apache.shiro.SecurityUtils;import org.apache.shiro.authc.IncorrectCredentialsException;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.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import javax.servlet.http.HttpServletRequest;/** * TODO your comment * * @author Yujiaqi * @date 2020/12/5 13:54 */@Controllerpublic class LoginController { @RequestMapping("/login") public String login(){ return "login"; } @RequestMapping("userLogin") public String userLogin(UserEntity userEntity, Model model){ // 获取主题用户 Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(userEntity.getUserName(), MD5Util.encode(userEntity.getPassWord())); try{ //登录就会走MayiktRealm.doGetAuthenticationInfo方法验证用户名密码 subject.login(usernamePasswordToken); model.addAttribute("userName" , userEntity.getUserName()); return "home"; }catch (IncorrectCredentialsException e){ e.printStackTrace(); model.addAttribute("error","账号密码错误!"); return "login"; } } }
MemberController类
/******************************************************************************* * Package: com.demo.shiro.controller * Type: MemberController * Date: 2020/12/5 11:06 * * Copyright (c) 2020 HUANENG GUICHENG TRUST CORP.,LTD All Rights Reserved. * * You may not use this file except in compliance with the License. *******************************************************************************/package com.demo.shiro.controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;/** * TODO your comment * * @author Yujiaqi * @date 2020/12/5 11:06 */@RestController@RequestMapping("/member")public class MemberController { @RequestMapping("getMember") public String getMember(){ return "getMember"; } @RequestMapping("insertMember") public String insertMember(){ return "insertMember"; }}
OrderController类
/******************************************************************************* * Package: com.demo.shiro.controller * Type: OrderController * Date: 2020/12/5 11:06 * * Copyright (c) 2020 HUANENG GUICHENG TRUST CORP.,LTD All Rights Reserved. * * You may not use this file except in compliance with the License. *******************************************************************************/package com.demo.shiro.controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;/** * TODO your comment * * @author Yujiaqi * @date 2020/12/5 11:06 */@RestController@RequestMapping("/order")public class OrderController { @RequestMapping("insertOrder") public String insertOrder(){ return "insertOrder"; } @RequestMapping("updateOrder") public String updateOrder(){ return "updateOrder"; } @RequestMapping("deleteOrder") public String deleteOrder(){ return "deleteOrder"; }}
前端页面
home.ftl
<html xmlns:th="http://www.thymeleaf.org"><head> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> <title>Insert title here</title></head><body> <h1>恭喜您进入到管理系统<span th:text="${userName}"></span></h1> <h3><a href="/order/insertOrder">insertOrder</a></h3> <h3><a href="/order/updateOrder">updateOrder</a></h3> <h3><a href="/order/deleteOrder">deleteOrder</a></h3> <h3><a href="/member/insertMember">insertMember</a></h3> <h3><a href=" /member/getMember" >getMember</a></h3></body></html>
login.ftl
<html xmlns:th="http://www.thymeleaf.org"><head> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> <title>Insert title here</title></head><body><h1>每特教育--权限控制登陆系统</h1><form action="/userLogin" method="post"> <span>用户名称:</span><input type= "text" name="userName"/><br> <span>用户密码:</span><input type="password" name= "passWord"/><br> <input type="submit" value="登陆" ></form> <span style="color: red" th:text="${error}"></span></body></html>
ShiroConfig类为启动时加载
我们访问127.0.0.1:8080/mayiktOpenApi可以直接访问到,因为在ShiroConfig类配置了map.put("/mayiktOpenApi","anon"); anon不需要登录,就可以访问接口
我们访问127.0.0.1:8080/home就会进入到127.0.0.1:8080/login登录页面,因为我们进行了拦截在ShiroConfig类配置了map.put("/home","authc");//authc需要要进行登录验证才可以访问到接口
我们输入错误的用户名密码进行登录:会进入到doGetAuthenticationInfo方法用来验证当前登录的用户,获取认证信息。用户名密码正确登录成功,否则登录失败,并且后台会报错
下面我们输入正确的用户名密码(登录成功)
我们登录的是yujiaqi_admin用户,他的权限为member,order,意思是我们可以访问/member/*接口也可以访问/order/*接口
接下来我们点击insertOrder(访问成功),会调用doGetAuthorizationInfo方法为当前登录成功的用户授予权限和分配角色。 在用户登录成功后,每次请求后台接口都需要先访问此方法,进行权限的验证
点击insertMember(访问成功)
其他访问类似
接下来我们登录yujiaqi_order用户,他的权限order说明mayikt_order用户,只能访问/order/*接口
我们点击insertOrder(访问成功)
点击insertMember(访问失败,没有权限)
到此我们就完成了shiro安全框架的整合,可以进行登录拦截,也可以给用户进行权限的分配
博主也是初学shiro框架,后续还会有Shiro整合Jwt实现前后端分离,Shiro整合0auth2实现权限认证正在学习,希望留下你的小心心
想当年我们也是在道上混的,只不过后来改邪归正了