Springboot+shiro 实战
1、Shiro概述(what)
Apache Shiro是一种功能强大且易于使用的Java安全框架,可执行身份验证,授权,加密和会话管理,可用于保护任何应用程序的安全- 从命令行应用程序,移动应用程序到最大的Web和企业应用程序。
1.1 功能概述
- 验证用户来核实他们的身份
- 对用户执行访问控制
- 判断用户是否被分配一个特定的角色
- 判定用户是否被允许做什么事
- 任何环境下都可以使用session API,即使没有web或者EJB容器
- 在身份验证,访问控制期间或者会话的声明周期,对事件做出反应
- 聚集一个或者多个用户的安全数据的数据源,并作为一个单一的复合用户视图
- 启动单点登录功能
- 启用Remember me 服务。
1.2 Shiro和Spring security的区别
-
shiro
shiro 相比较于spring security 在保持强大功能的同时,具有简单和灵活优势。
- 易于理解的Java security API 。
- 简单的身份认证(登录),支持多多种数据源(LDAP、JDBC、Kerberos、ActiveDirectory等)。
- 对角色的简单鉴权(访问控制),支持细粒度鉴权。
- 内置基于POJO的企业会话管理,适用于web和非web环境
- 异构客户端会话访问
- 非常简单的加密API
- 不和任何框架捆绑,可以独立运行
-
security
- 优点:
1. 对oauth、openID有支持,shiro需要手动实现
2. 权限粒度更细 - 缺点
1. 必须和spring结合使用
- 优点:
1.3 shiro的功能模块
认证、授权、加密、会话管理、与web集成和会话管理、API简单、可以独立运行,功能如下:
2、Shiro工作原理(why)
2.1 shiro的架构理解
- Subject:主体,可以看到主体可以是任何可以与应用交互的“用户”;
- SecurityManager : 相 当 于 SpringMVC 中 的 DispatcherServlet 或 者 Struts2 中的 FilterDispatcher,是 Shiro 的核心;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。
- Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
- Authrizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
- Realm:可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是 JDBC 实现,也可以是 LDAP 实现,或者缓存实现等等,由用户提供。注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的 Realm;
- SessionManager:会话管理器,如果写过 Servlet 就应该知道 Session 的概念,Session 呢需要有人去管理它的生命周期,这个组件就是 SessionManager;而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境、EJB 等环境;所有呢,Shiro 就抽象了一个自己的 Session来管理主体与应用之间交互的数据;这样的话,比如我们在 Web 环境用,刚开始是一台Web 服务器;接着又上了台 EJB 服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到 Redis服务器);
- SessionDAO:DAO 大家都用过,数据访问对象,用于会话的 CRUD,比如我们想把 Session保存到数据库,那么可以实现自己的 SessionDAO,通过如 JDBC 写到数据库;比如想把Session 放到 Memcached/Redis 中,可以实现自己的 Memcached/Redis SessionDAO;另外 SessionDAO中可以使用 Cache 进行缓存,以提高性能;
- CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本
上很少去改变,放到缓存中后可以提高访问的性能 - Cryptography:密码模块,Shiro 提高了一些常见的加密组件用于如密码加密
2.2 Realm 域
一般继承 AuthorizingRealm(授权)即可;其继承了 AuthenticatingRealm(即身份验证),而且也间接继承了 CachingRealm(带有缓存实现),重写里面AuthenticationInfo 认证和doGetAuthorizationInfo授权方法。
2.3 应用程序使用shiro
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pchHc7NL-1587288125363)(图片/shiro-应用.png)]
2.4 认证流程
2.5 授权流程
2.3 shiro 拦截规则
3、应用场景(who\where\when)
3.1、特点
Apache Shiro旨在成为最全面,但也最容易使用的Java安全框架。以下是一些框架要点:
(1)在任何地方最容易理解的Java安全性API。类和接口的名称很直观并且很有意义。任何东西都是可插入的,但是所有东西都存在良好的默认值。
(2)支持跨一个或多个可插拔数据源(LDAP,JDBC,ActiveDirectory等)的身份验证(“登录”)。
(3)还可使用可插拔数据源,根据角色或细粒度权限执行授权(“访问控制”)。
(4)一流的缓存支持可增强应用程序性能。
(5)内置基于POJO的企业会话管理。可在Web和非Web环境中使用,也可在需要单点登录(SSO)或群集或分布式会话的任何环境中使用。
(6)异构客户端会话访问。您不再被迫仅使用httpSession或有状态会话Bean,它们常常不必要地将应用程序绑定到特定环境。现在,无论部署环境如何,Flash Applet,C#应用程序,Java Web Start和Web应用程序等都可以共享会话状态。
(7)简单单点登录(SSO)支持附带上述企业会话管理。如果会话是跨多个应用程序联合的,则用户的身份验证状态也可以共享。一次登录到任何应用程序,其他都可以识别该登录。
(8)使用最简单的可用加密API保护数据安全,从而为您提供强大的功能和简便性,而Java默认情况下不提供加密和哈希功能。
(9)一个非常健壮但配置低的Web框架,可以保护任何URL或资源,自动处理登录和注销,执行“记住我”服务等等。
(10)所需依赖项的数量极少。独立配置仅需要slf4j-api.jar和slf4j的绑定.jars之一。 Web配置还需要commons-beanutils-core.jar。可以在需要时添加基于功能的依赖项(Ehcache缓存,基于Quartz的会话验证,Spring依赖项注入等)。
4、实战(how)
4.1 依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.5.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.5.2</version>
</dependency>
<!--开源组件:shiro redis管理shiro会话,自定义shiro会话
升版本:AuthCachePrincipal就会找不到-->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.0.0</version>
</dependency>
4.2 配置文件
spring:
# 应用配置
application:
name: shiro_springboot
# 数据库连接池
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.35.80:3306/shiro_test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: xxx
password: xxx
redis:
host: 192.168.35.80
port: 6379
password: xxx
database: 1
4.3 配置类
import com.sd.shiro.session.CustomerSessionManager;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPool;
import java.util.LinkedHashMap;
/**
* @author 三多
* @Time 2020/4/2
*/
@Configuration
public class ShiroConfig {
/**
* 配置shiro的过滤器工厂
* 一组过滤器集合
* 拦截页面跳转
* 步骤:
* 1.创建过滤器工厂
* 2.设置安全管理器
* 3.通用配置(跳转登录页面,未授权跳转的页面)
* 4.设置过滤器集合
* 问题:web.xml 没有配置shiro相关的拦截器
* 解决问题:
* org.apache.shiro.UnavailableSecurityManagerException:
* No SecurityManager accessible to the calling code,
* either bound to the org.apache.shiro.util.
* ThreadContext or as a vm static singleton.
* This is an invalid application configuration.
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager ) {
// 1.创建过滤器工厂
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 2.设置安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
//3.通用配置(跳转登录页面,未授权跳转的页面)
//跳转url
shiroFilterFactoryBean.setLoginUrl("/authError?code=1");
//未授权url
shiroFilterFactoryBean.setUnauthorizedUrl("/authError?code=2");
// 在 Shiro过滤器链上加入 JWTFilter
/*LinkedHashMap<String, Filter> filters = new LinkedHashMap<>();
filters.put("jwt", new JWTFilter());
shiroFilterFactoryBean.setFilters(filters);*/
//4.设置过滤器集合,map集合,设置所有的过滤器有顺序:key:拦截的地址;value:过滤器类型
LinkedHashMap<String, String> filterMap= new LinkedHashMap<>();
//a.当前请求可以匿名访问
//filterMap.put("/user/home","anon");
//b.具有某种权限才可以访问,不具备,会跳转到UnauthorizedUrl地址
//filterMap.put("/user/home","perms[user-home]");
//c.使用过滤器过滤角色(可以使用注解方式)
//filterMap.put("/user/home","roles[系统管理员]");
filterMap.put("/user/login","anon");
//filterMap.put("/user/find","anon");
//当前请求地址必须认证后访问
filterMap.put("/user/**","authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
/**
* 创建shiro安全管理器
* 管理所有的realm
* @return
*/
@Bean
public SecurityManager securityManager(CustomerRealm customerRealm) {
//使用web默认的额缓存管理器,可以自定义使用外部的缓存管理器
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 配置 SecurityManager,并注入 shiroRealm
securityManager.setRealm(customerRealm);
//可选:设置自定义的会话管理器
securityManager.setSessionManager(sessionManager());
//可选:设置自定义的缓存管理器
securityManager.setCacheManager(redisCacheManager());
return securityManager;
}
/**
* 创建realm
* @return
*/
@Bean
public CustomerRealm shiroRealm() {
// 配置 Realm
return new CustomerRealm();
}
/**
* 自定义Session会话
* 1、redis的控制器、操作redis
* 2、SessionDao
* 3、会话管理器
* 4、缓存管理器
*/
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.database}")
private int database;
/***
* 1、redis的控制器、操作redis
* @return
*/
public RedisManager redisManager(){
RedisManager redisManager = new RedisManager();
/*GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig<>();
poolConfig.setMaxIdle(100);
poolConfig.setMaxTotal(100);
poolConfig.setMinIdle(0);
JedisPool jedisPool = new JedisPool(poolConfig,host,port);
redisManager.setJedisPool(jedisPool);*/
redisManager.setHost(host);
redisManager.setPort(port);
redisManager.setDatabase(database);
redisManager.setPassword(password);
return redisManager;
}
/**
* 2、SessionDao
* @return
*/
public RedisSessionDAO redisSessionDAO(){
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
/**
* 3、会话管理器
* @return
*/
public DefaultWebSessionManager sessionManager(){
CustomerSessionManager sessionManager = new CustomerSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}
/**
* 4、缓存管理器
* @return
*/
public RedisCacheManager redisCacheManager(){
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
/**
* 开启对shiro注解的支持
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
4.4 自定义realm
import com.sd.shiro.domain.entity.Permission;
import com.sd.shiro.domain.entity.Role;
import com.sd.shiro.domain.entity.User;
import com.sd.shiro.service.IUserService;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
/**
* @author 三多
* @Time 2020/4/2
*/
//@Component("customerRealm")
public class CustomerRealm extends AuthorizingRealm {
@Override
public void setName(String name){
super.setName("customerRealm");
}
@Autowired
private IUserService userService;
/**
* 授权
* 判断用户是否具有相应的权限
* 1.先认证----安全数据
* 2.在授权----根据安全数据获取用户相应的操作权限
* 1.获取已认证的用户数据
* 2.获取用户的权限(角色,权限)
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 1.获取已认证的用户数据
User user = (User) principals.getPrimaryPrincipal();
//2.获取用户的权限(角色,权限)
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
Set<String> roles = new HashSet<String>();
Set<String> perms = new HashSet<String>();
for (Role role : user.getRoles()) {
//角色
roles.add(role.getName());
for (Permission perm : role.getPermissions()) {
//权限
perms.add(perm.getCode());
}
}
info.setRoles(roles);
info.setStringPermissions(perms);
return info;
}
/**
* 认证
* 1.获取用户名密码
* 2.根据用户名查询
* 3.判断密码是否一致
* 4.如果一致返回安全数据
* 5.不一致,返回null,或者抛异常
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//1.获取用户名密码
UsernamePasswordToken passwordToken = (UsernamePasswordToken)token;
String username = passwordToken.getUsername();
String password = new String(passwordToken.getPassword());
// 2.根据用户名查询
User user = userService.findByUsername(username);
//3.判断密码是否一致
if(Objects.nonNull(user) && user.getPassword().equals(password)){
// 4.如果一致返回安全数据 用户数据,密码,realm域名称
AuthenticationInfo info = new SimpleAuthenticationInfo(user,user.getPassword(),this.getName());
return info;
}
// 5.不一致,返回null,或者抛异常
return null;
}
}
4.5 自定义session管理器
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;
/**
* 自定义 sessionManager
*
* @author 三多
* @Time 2020/4/14
*/
public class CustomerSessionManager extends DefaultWebSessionManager {
private static final String AUTHOR_HEADER = "Authorization";
public static final String HEADER_SESSION_ID_SOURCE = "header";
/**
* 头信息中具有sessionID
* 请求头:Authorization:sessionId
* 指定sessionId的获取方式
*
* @param request
* @param response
* @return
*/
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
//获取请求头中Authorization数据。sessionId
String id = WebUtils.toHttp(request).getHeader(AUTHOR_HEADER);
if (StringUtils.isEmpty(id)) {
//如果没有生成新的sessionId
return super.getSessionId(request, response);
} else {
//请求头信息 :Bearer sessionId
id = id.replace("Bearer ","");
//返回SessionId
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
HEADER_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
}
}
}