系列文章
SpringBoot 01 —— HelloSpringBoot、yaml配置、数据校验、多环境切换
SpringBoot 02 —— Web简单探究、员工管理系统
SpringBoot 03 —— Spring Security
SpringBoot 04 —— Shiro
十二、Shiro
12.1、介绍
- Apache Shiro 是一个Java的安全(权限)框架
- Shiro可以非常容易的开发出足够好的应用,它不仅可以用在JavaSE环境,也可以用在JavaEE环境。
- Shiro可以完成认证、授权、加密、会话管理、Web集成、缓存等功能。
官网:https://shiro.apache.org/
GitHub:https://github.com/apache/shiro
详细功能:
- Authentication(验证):进行身份验证、判断能否登录以及验证用户是否拥有相应权限
- Authorization(授权):进行授权
- Session Manager:会话管理,即用户登录后就是第一次会话,在没有推出前,它的信息都在会话中;类似JavaWeb的Session,而Shiro能在JavaSE环境也提供。
- Cryptography:加密,保护数据的安全性;可以把密码加密后再存储到数据库中。
- Web Support:Web支持,可以很容易集成到Web环境。
- Caching:缓存,比如用户登录后,它的用户信息、权限等都不必每次去重新查询,这样能提高效率。
- Concurrency:Shiro支持多线程应用的并发验证,即,在一个线程中开启另一个线程,就能把权限自动传播过去。
- Testing:提供测试支持。
- Run As:允许一个用户假装为另一个用户(如果他同意)的身份进行访问。
- Remember Me:记住我。即登录一次后,下次自动登录。
Shiro架构:
- Subject 对象:表示当前用户。和代码进行交互的就是Subject对象,它也是Shiro的对外API核心。Subject表示的用户不一定是个具体的人,而是说于当前应用交互的任何东西都是Subject,例如网络爬虫、机器人,与Subject的所有交互都会委托给SecurityManager进行管理。
- SecurityManager 对象:安全管理器,所有与安全相关的操作都会涉及到SecurityManager,同时它也管理着所有的Subject,是Shiro的核心。它负责与Shiro的其他组件进行交互,相当于SpringMVC的DispatcherServlert。
- Realm 对象:Shiro会从Realm获取安全数据(用户、角色、权限),也就是说SecurityManager要验证用户的身份需要从Realm获取相应的用户来进行比较,也需要从Realm得到用户相应的角色、权限等信息,来验证用户的操作是否可以进行下去。可以把Realm看成DataSource。
12.2、快速体验
这是官方提供的一个Sample,运行试试看,后面会具体讲解。
1、创建一个普通的Maven项目
2、导入依赖
<dependencies>
<!-- Shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.7.25</version>
</dependency>
<!-- 这个是日志门面,通过它能调用很多其他日志框架 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
</dependencies>
放在resources目录下:
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 (如果IDEA对.ini文件没有高亮,需要导入ini插件,去setting-plugins里安装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
3、Quickstart.java
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Quickstart {
private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);
public static void main(String[] args) {
// 创建带有配置的Shiro SecurityManager的最简单方法
// 领域,用户,角色和权限是使用简单的INI配置
// 我们将使用可提取.ini文件的工厂来完成此操作,返回一个SecurityManager实例:
// 在类路径的根目录下使用shiro.ini文件(file:和url:前缀分别从文件和url加载):
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
//对于这个简单的示例快速入门,请使SecurityManager作为JVM单例访问。
//大多数应用程序都不会这样做,而是依靠其容器配置或web.xml进行webapps。
//这超出了此简单快速入门的范围,因此我们只做最低限度的工作,所以你可以继续感受一下。
SecurityUtils.setSecurityManager(securityManager);
// 现在已经建立了一个简单的Shiro环境,让我们看看您可以做什么:
// 获取当前执行的用户:
Subject currentUser = SecurityUtils.getSubject();
// 使用Session做一些事情(不需要Web或EJB容器!!!)
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 + "]");
}
// 判断当前用户是否被认证(授权)
if (!currentUser.isAuthenticated()) {
//token:即令牌
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
token.setRememberMe(true);
try {
currentUser.login(token);//执行登录操作
} catch (UnknownAccountException uae) {
log.info("用户名不存在:" + token.getPrincipal());
} catch (IncorrectCredentialsException ice) {
//
log.info("密码错误:" + token.getPrincipal());
} catch (LockedAccountException lae) {
log.info("用户被锁定了" + token.getPrincipal());
}
catch (AuthenticationException ae) {
//未知错误
}
}
//说出他们是谁:打印其标识主体(在这种情况下,为用户名):
log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");
//判断用户是否有相应权限(在shiro.ini里设置的)
if (currentUser.hasRole("schwartz")) {
log.info("May the Schwartz be with you!");
} else {
log.info("Hello, mere mortal.");
}
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.");
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!");
//注销
currentUser.logout();
//结束系统
System.exit(0);
}
}
4、直接运行
12.3、SpringBoot集成Shiro
源码下载:
- CSDN:https://download.csdn.net/download/qq_39763246/16264048
- 百度云:https://pan.baidu.com/s/1BpMzuOHEKZknH52FmdsjAg 提取码: kdin
项目结构:
需要提前建好数据库用户表:
实现后的效果:
-
访问主页
-
点击登录
-
注销,登录小红
-
注销后,直接复制URL进行登录,被拦截。
所导入的依赖:
<!--thymeleaf—— 命名空间xmlns:th="http://www.thymeleaf.org" -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- lombok简化pojo -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
<!-- Shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!-- LOG4J日志 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!-- Druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.5</version>
</dependency>
<!-- mybatis的启动器 -->
<dependency>
<groupId> org.mybatis.spring.boot </groupId>
<artifactId> mybatis-spring-boot-starter </artifactId>
<version> 2.1.3 </version>
</dependency>
1、配置Shiro
-
编写UserReaml类
作用:正如前面介绍部分所说,Realm是Shiro三大对象之一,它用于向SecurityManager提供用户信息,主要就是认证和授权方法。认证方法,即用户登录时会进入该方法判断用户名、密码等。授权方法,即用户登录时会进入该页面判断用户权限是否足够。
//自定义的UserRealm public class UserRealm extends AuthorizingRealm { @Autowired UserServiceImpl userService; //授权(需要用户有相应权限才能访问) @Override protected AuthorizationInfo doGetAuthorizationInfo( PrincipalCollection principals) { System.out.println("执行了——>授权 doGetAuthorizationInfo"); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); //对当前用户进行判断并授予相应权限 Subject subject = SecurityUtils.getSubject();//获得当前用户 User currentUser = (User) subject.getPrincipal();//拿到当前User对象,是认证里传过来的 //该User是从数据库中读取的,User的Perms是该用户拥有的权限。多个权限用|分割。 String[] strings = currentUser.getPerms().split("\\|"); if(strings!=null){ ArrayList<String> arrayList = new ArrayList<>(); for (String string : strings) { arrayList.add(string); } //授予当前用户的权限 info.addStringPermissions(arrayList); } return info; } //认证(判断用户名和密码是否正确) @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token) throws AuthenticationException { System.out.println("执行了——>认证 doGetAuthenticationInfo"); UsernamePasswordToken userToken = (UsernamePasswordToken)token; //用户名认证 User user = userService.queryUserByName(userToken.getUsername()); if(user == null){ //用户是否存在 return null;//如果用户名不存在,则返回null,即会抛出异常 UnknownAccountException } //密码认证,由Shiro自己完成。可以进行加密。第一个参数 可以让其他方法拿到 return new SimpleAuthenticationInfo(user, user.getPassword(), ""); } }
-
编写ShiroConfig类
作用:这个类包含了Shiro三大对象的SecurityManager,在
ShiroFilterFactoryBean
方法里,我们可以设置拦截或者要求访问某些页面需要对应权限,以及被拦截后跳转的新页面(和SpringSecurity类似的)。后面两个方法DefaultWebSecurityManager
和UserRealm
都是连贯着的,具体看注释。@Configuration public class ShiroConfig { //3. ShiroFilterFactoryBean,后续的操作在这里进行添加,例如登录拦截 @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){ ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); //设置安全管理器 bean.setSecurityManager(securityManager); /** 添加Shiro的内置过滤器,进行登录拦截。 * anon:无需认证就能访问 * authc:必须认证才能访问 * user:必须拥有"记住我"功能才能访问,一般不用 * perms:拥有"对某个资源的权限"才能访问 * role:拥有某个角色权限才能访问 */ Map<String, String> filterMap = new LinkedHashMap<>(); //设置add和update页面为需要相应权限才能访问 filterMap.put("/user/add", "perms[user:add]"); filterMap.put("/user/update", "perms[user:update]"); //注意!!! 要想设置权限,必须把拦截写在权限的下面,也就是下面这行代码必须在上面里昂行代码下面!! filterMap.put("/user/*", "authc");//使得/user/下的所有请求都需要认证了才能访问 bean.setFilterChainDefinitionMap(filterMap); //设置权限不足的跳转页面 bean.setUnauthorizedUrl("/unauthorized"); //设置登录的请求,当被拦截时,跳转到登录页面 bean.setLoginUrl("/toLogin"); return bean; } //2. DefaultWebSecurityManager, // 注解@Qualifier 是按名称进行注入,Spring会通过userRealm()方法返回一个UserRealm对象。 @Bean(name = "securityManager") public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); //关联UserRealm securityManager.setRealm(userRealm); return securityManager; } //1. 创建Realm对象,返回Bean,需要先自定义类。 @Bean(name = "userRealm")//可省略name,默认就是通过userRealm()方法名来获取 public UserRealm userRealm(){ return new UserRealm(); } //利用ShiroDialect来整合Thymeleaf,这个方法是为了在前端使用Shiro命名空间。 @Bean public ShiroDialect getShiroDialect(){ return new ShiroDialect(); } }
2、MyBatis
-
配置MyBatis和Druid数据源(可以不用这个数据源,就用默认的,具体看 员工管理系统的准备工作 ),使用yaml
spring: datasource: username: root password: '123456' #一般需要增加时区配置:serverTimezone=Asia/Shanghai url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai #SpringBoot现在推荐这个驱动 com.mysql.jdbc.Driver driver-class-name: com.mysql.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource #切换到Druid数据源 #Spring Boot 默认是不注入这些属性值的,需要自己绑定(写Druid配置类) #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.zcy.pojo mapper-locations: classpath:mapper/*.xml
-
dao层
@Mapper @Repository public interface UserMapper { User queryUserByName(String name); }
<?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.zcy.dao.UserMapper"> <select id="queryUserByName" parameterType="String" resultType="User"> select * from user where name=#{ name}; </select> </mapper>
-
service层
public interface UserService { User queryUserByName(String name); }
@Service public class UserServiceImpl implements UserService { @Autowired UserMapper userMapper; @Override public User queryUserByName(String name) { return userMapper.queryUserByName(name); } }
-
controller层
@Controller public class MyController { @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){ //获取当前用户,Subject对象就代表当前用户 Subject subject = SecurityUtils.getSubject(); //封装用户的登录数据(将前端传进来的用户名和密码放入token里) UsernamePasswordToken token = new UsernamePasswordToken(username, password); try { subject.login(token); model.addAttribute("username", username); //登录成功则进入主页,失败则抛出异常并返回登录页 return "index"; }catch (UnknownAccountException e){ //用户名不存在 model.addAttribute("msg", "用户名错误"); return "login"; }catch (IncorrectCredentialsException e){ //密码错误 model.addAttribute("msg", "用户密码错误"); return "login"; } } @ResponseBody @RequestMapping("/unauthorized") public String unauthorized(){ return "权限不足"; } @RequestMapping("/logout") public String logout(){ Subject subject = SecurityUtils.getSubject(); subject.logout(); return "login"; } }
3、前端
-
index.html
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro"> <head> <meta charset="UTF-8"> <title>首页</title> </head> <body> <h1>首页</h1> <!-- 未认证,即未登录时就显示登录按钮 --> <div shiro:notAuthenticated> <a href="/toLogin">登录</a> </div> <!-- 已认证,即已登录时就显示注销按钮 --> <div shiro:authenticated=""> <h2>欢迎<span th:text="${username}"></span></h2> <a href="/logout">注销</a> </div> <p th:text="${msg}"></p> <!-- 有add权限时才显示该链接 --> <div shiro:hasPermission="user:add"> <a th:href="@{/user/add}">add</a> </div> <!-- 有update权限时才显示该链接 --> <div shiro:hasPermission="user:update"> <a th:href="@{/user/update}">update</a> </div> </body> </html>
-
login.html
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <!-- 新增一个提示信息 --> <p th:text="${msg}" style="color:red;"></p> <form th:action="@{/login}"> <p>用户名;<input type="text" name="username"></p> <p>密码;<input type="text" name="password"></p> <p><input type="submit" value="提交"></p> </form> </body> </html>
-
add.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>add</h1> <a href="/logout">注销</a> </body> </html>
-
update.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>update</h1> <a href="/logout">注销</a> </body> </html>