Shiro
此部分内容参考【狂神说 Java】SpringBoot 最新教程 IDEA 版通俗易懂[1]以及学习笔记[2],仅供学习使用!
1、Shiro 简介
Apache Shiro (pronounced “shee-roh”, the Japanese word for ‘castle’) is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management and can be used to secure any application - from the command line applications, mobile applications to the largest web and enterprise applications.
是的,又看到 ==powerful==了,就像Spring Security
一样
-
Apache Shiro 是一个 Java 的 安全(权限)框架
。 -
Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境。 -
Shiro 可以完成, 认证
,授权
,加密
,会话管理
,Web集成
,缓存
等.
2、Shiro 测试环境搭建
新建一个Spring Boot
项目spring-boot-08-shiro
,添加web
、thymeleaf
框架。
2.1、导入 shiro 依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.8.0</version>
</dependency>
2.2、编写 shiro 配置类
在配置类中需要注入三个 Bean:ShiroFilterFactoryBean
、DefaultWebSecurityManager
、UserRealm
。
第一步:创建 Realm 对象,需要自定义类继承 AuthorizingRealm,重写认证和授权部分方法。
config.UserRealm.java
package com.thomas.config;
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;
public class UserRealm extends AuthorizingRealm {
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了===>授权");
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了========>认证");
return null;
}
}
将Realm
对象注入到配置类中
config.ShiroConfig
package com.thomas.config;
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;
@Configuration
public class ShiroConfig {
@Bean
public UserRealm userRealm() {
return new UserRealm();
}
}
第二步:将UserRealm
对象设置到defaultWebSecurityManager
@Bean(name = "webSecurityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier(value = "userRealm") UserRealm userRealm) {
DefaultWebSecurityManager webSecurityManager = new DefaultWebSecurityManager();
webSecurityManager.setRealm(userRealm);
return webSecurityManager;
}
第三步:将defaultWebSecurityManager
设置到ShiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier(value = "webSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
return shiroFilterFactoryBean;
}
2.3、编写 controller
在 controller 层,我们实现简单的页面跳转功能。跳转首页,跳转/usr/update
, /usr/add
package com.thomas.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class MyController {
@GetMapping({"/", "/index"})
public String toIndex(Model model) {
model.addAttribute("msg", "Hello, shiro");
return "index";
}
@GetMapping("/usr/add")
public String add() {
return "user/add";
}
@GetMapping("/usr/update")
public String update() {
return "user/update";
}
}
2.4、编写前端页面
index.html
<!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>Document</title>
</head>
<body>
<h1>首页</h1>
<p th:text="${msg}"></p>
<hr>
<a th:href="@{/usr/add}">add</a> | <a th:href="@{/usr/update}">update</a>
</body>
</html>
user/add.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>add</h1>
</body>
</html>
user/update.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>update</h1>
</body>
</html>
测试,因为没有设置认证,所以均可以访问。

3、Shiro 实现登录拦截
shiro
有内置过滤器,是通过DefaultFilter
枚举类来实现的,在其中定义了常见的过滤器,我们可以借助这些过滤器来实现拦截请求。
3.1、设置登录验证
我们在ShiroFilterFactoryBean
中设置添加 shiro 的内置过滤器,将前面的两个页面设置为authc
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier(value = "webSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
//添加shiro的内置过滤器
/*
* anon: 无需认证就可以访问
* authc: 必须认证了才能访问
* user: 必须拥有 记住我 功能才能用
* perms: 拥有对某个资源的权限才能访问
* role: 拥有某个角色权限才能访问
* */
Map<String, String> map = new HashMap<>();
//map.put("/usr/add", "authc");
//map.put("/usr/update", "authc");
//支持通配符
map.put("/usr/*", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
重新启动项目,测试发现无法访问,此时我们的请求被成功拦截。
3.2、登录页面
当页面需要登录权限而用户未登录时,此时应该跳转到登录页面。
login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<h1>登录</h1>
<hr>
<form action="">
<p>用户名: <input type="text" name="username"></p>
<p>密码: <input type="password" name="password"></p>
<p><input type="submit"></p>
</form>
跳转请求
package com.thomas.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class MyController {
...
@RequestMapping("/toLogin")
public String toLogin() {
return "login";
}
}
设置登录页面
package com.thomas.config;
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.HashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
//ShiroFilterFactoryBean:第三步
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier(value = "webSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
//添加shiro的内置过滤器
/*
* anon: 无需认证就可以访问
* authc: 必须认证了才能访问
* user: 必须拥有 记住我 功能才能用
* perms: 拥有对某个资源的权限才能访问
* role: 拥有某个角色权限才能访问
* */
Map<String, String> map = new HashMap<>();
// map.put("/usr/add", "authc");
// map.put("/usr/update", "authc");
map.put("/usr/*", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
//设置登录页面
shiroFilterFactoryBean.setLoginUrl("/toLogin");
return shiroFilterFactoryBean;
}
...
}
测试,访问/usr/add
,/usr/update
页面即可实现跳转到登录页面。
4、Shiro 实现用户认证
4.1、用户认证
用户认证功能主要是通过Realm
对象来实现的。我们先编写/login
请求,封装用户登录数据,执行登录方法
@RequestMapping("/login")
public String login(String username, String password, Model model) {
//获取当前的用户
Subject subject = SecurityUtils.getSubject();
//封装用户的登录数据
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
subject.login(token); //执行登录方法,如果没有异常就说明登录成功
return "index";
} catch (UnknownAccountException e) { //用户名不存在
model.addAttribute("msg", "用户名错误");
return "login";
} catch (IncorrectCredentialsException e) { //密码错误
model.addAttribute("msg", "密码错误");
return "login";
}
}
login.html
绑定 action,显示信息
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<h1>登录</h1>
<p th:text="${msg}" style="color: red"></p>
<hr>
<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>
重新启动,点击登录,在控制台可以看到执行了Realm
中的认证。
4.2、用户身份认证
用户实现登录功能,还需要进行校验,在UserRealm
中完成认证功能。
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了========>认证");
//用户名,密码 从数据中取
String name = "root";
String password = "123456";
UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;
if (!userToken.getUsername().equals(name)) {
//用户名不一致
return null; //抛出异常 UnknownAccountException
}
//密码验证,交给shiro实现
return new SimpleAuthenticationInfo("", password, "");
}
启动测试,只有用户名和密码正确才能登录。
5、Shiro 整合 Mybatis
5.1、导入相关包
包括druid
数据源、jdbc
、mybatis
、mysql
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.17</version>
</dependency>
<!-- https://mvnrepository.com/artifact/log4j/log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
5.2、配置 mybatis 以及数据源
application.yml
spring:
datasource:
username: 用户名
password: 密码
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&useUnicode=true&characterEncoding=utf-8
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
application.properties
mybatis.type-aliases-package=com.thomas.pojo
mybatis.mapper-locations=classpath:mapper/*.xml
5.3、编写实体类、mapper 相关接口和实现类
pojo/User.java
package com.thomas.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private int id;
private String name;
private int age;
private String password;
}
mapper/UserMapper.java
(接口)
package com.thomas.mapper;
import com.thomas.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
@Repository
@Mapper
public interface UserMapper {
public User queryUserByName(String name);
}
Resources/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.thomas.mapper.UserMapper">
<select id="queryUserByName" resultType="User">
select * from mybatis.user where `name` = #{name}
</select>
</mapper>
service/UserService.java
(接口)
package com.thomas.service;
import com.thomas.pojo.User;
public interface UserService {
public User queryUserByName(String name);
}
service/UserServiceImpl.java
package com.thomas.service;
import com.thomas.mapper.UserMapper;
import com.thomas.pojo.User;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class UserServiceImpl implements UserService {
@Resource
UserMapper userMapper;
@Override
public User queryUserByName(String name) {
return userMapper.queryUserByName(name);
}
}
最后我们来测试一下:
5.4、在 Shiro 中使用 mybatis
从数据库中获取用户名和密码。
package com.thomas.config;
import com.thomas.mapper.UserMapper;
import com.thomas.pojo.User;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.StringUtils;
import javax.annotation.Resource;
public class UserRealm extends AuthorizingRealm {
@Resource
UserMapper userMapper;
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了===>授权");
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了========>认证");
UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;
//用户名,密码 从数据库中取
User user = userMapper.queryUserByName(userToken.getUsername());
if (user == null) {
//用户名不一致
return null; //抛出异常 UnknownAccountException
}
//密码验证,交给shiro实现
return new SimpleAuthenticationInfo("", user.getPassword(), "");
}
}
我们使用数据库中的用户名进行登录。
6、Shiro 完成授权
6.1、拦截授权
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier(value = "webSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
//添加shiro的内置过滤器
/*
* anon: 无需认证就可以访问
* authc: 必须认证了才能访问
* user: 必须拥有 记住我 功能才能用
* perms: 拥有对某个资源的权限才能访问
* role: 拥有某个角色权限才能访问
* */
Map<String, String> map = new HashMap<>();
// map.put("/usr/add", "authc");
// map.put("/usr/update", "authc");
//授权,正常的情况下,没有授权会跳转到未授权页面
map.put("/usr/add", "perms[user:add]");
map.put("/usr/*", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
//设置登录页面
shiroFilterFactoryBean.setLoginUrl("/toLogin");
return shiroFilterFactoryBean;
}
这里我们进行授权,perms[user:add]
表示必须是user
用户有add
权限才能授权访问。
但是我们登录之后访问localhost:8080/usr/add
,仍然能正常访问。其实问题就在于,我们在ShiroFilterFactoryBean
中先过滤再授权了,这样导致所有的请求都通过了。这时可能有小伙伴发现了,不对呀,我们明明是先写的map.put("/usr/add", "perms[user:add]");
,然后才写的map.put("/usr/*", "authc");
,这样应该是先授权了再过滤呀。
**这其实是因为 HashMap 不保证插入顺序,我们这里需要按照顺序插入 key,因此需要采用LinkedHashMap
**,从下图也能看出两者的区别。

我们将上面的HashMap
改为LinkedHashMap
。重新启动,因为没有授权,所以无法访问 add 页面。
6.2、设置未授权页面
当用户未授权时,应该跳转到未授权页面,所以我们需要设置相关页面。
controller层
@RequestMapping("/unauthorized")
@ResponseBody
public String toUnAuthorized() {
return "未经授权无法访问此页面";
}
ShiroConfig.java
//设置未授权请求
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
6.3、设置授权
我们先修改一下数据库的结构,加入一列perms
,用来表示用户的授权情况,对于root
用户,我们给予两个页面的权限
修改一下对应的实体类,加入 perms 字段。
package com.thomas.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private int id;
private String name;
private int age;
private String password;
private String perms;
}
我们应该如何给当前用户设置权限呢?还记得之前说过的Shiro
的三大对象吗,我们可以通过Subject
来获得当前对象。在认证阶段,通过认证后我们返回User
对象
...
public class UserRealm extends AuthorizingRealm {
@Resource
UserMapper userMapper;
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
...
//密码验证,交给shiro实现
return new SimpleAuthenticationInfo(user, user.getPassword(), "");
}
}
在授权时,我们通过 Subject 获取 User 对象,然后查询到数据库中对应用户的权限。这里我们需要考虑权限设置为 null 的情况以及多个权限的情况。
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了===>授权");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
Subject subject = SecurityUtils.getSubject();
User currentUser = (User) subject.getPrincipal(); //拿到User对象
//使用当前数据库中查询出来的权限
String perms = currentUser.getPerms();
if (perms == null) {
return null;
}
//可能存在多个权限
//添加权限
Arrays.stream(perms.split(",")).forEach(info::addStringPermission);
return info;
}
将两个页面的请求都加入到拦截中。
Map<String, String> map = new LinkedHashMap<>();
map.put("/usr/add", "perms[user:add]");
map.put("/usr/update", "perms[user:update]");
重新启动,此时:
-
root 用户可以访问 add 和 update 页面 -
thomas1 用户仅可访问 add 页面 -
其他用户无权限
总结
以上就是关于 Shiro 的学习内容了。从功能上看,Shiro 提供了与 SpringSecurity 高度相似的功能,相对而言,Shiro 会更简单,容易上手。
参考资料
【狂神说 Java】SpringBoot 最新教程 IDEA 版通俗易懂: https://www.bilibili.com/video/BV1PE411i7CV/?vd_source=f5b96c50144e9d1797a8c9056fefba8a
[2]
学习笔记: https://github.com/lzh66666/SpringBoot
原文始发于微信公众号(多肉罗罗):SpringBoot学习笔记(三)—— Shiro
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/186214.html