一、概述
什么是Apache Shiro
Apache Shiro是一个功能强大且灵活的开源安全框架,可以清晰地处理身份验证,授权,企业会话管理和加密。
Apache Shiro的首要目标是易于使用和理解。安全有时可能非常复杂,甚至是痛苦的,但事实并非如此。框架应尽可能掩盖复杂性,并提供简洁直观的API,以简化开发人员确保其应用程序安全的工作。
以下是Apache Shiro可以做的一些事情:
验证用户以验证其身份
为用户执行访问控制,例如:
确定是否为用户分配了某个安全角色
确定是否允许用户执行某些操作
在任何环境中使用Session API,即使没有Web容器或EJB容器也是如此。
在身份验证,访问控制或会话生命周期内对事件做出反应。
聚合用户安全数据的1个或多个数据源,并将其全部显示为单个复合用户“视图”。
启用单点登录(SSO)功能
无需登录即可为用户关联启用“记住我”服务
......
以及更多 - 全部集成到一个易于使用的内聚API中。
Shiro尝试为所有应用程序环境实现这些目标 - 从最简单的命令行应用程序到最大的企业应用程序,而不会强制依赖其他第三方框架,容器或应用程序服务器。当然,该项目旨在尽可能地融入这些环境,但它可以在任何环境中开箱即用。
Apache Shiro功能
四大核心模块
身份验证:有时称为“登录”,这是证明用户是他们所说的人的行为。
授权:访问控制的过程,即确定“谁”可以访问“什么”。
会话管理:即使在非Web或EJB应用程序中,也可以管理特定于用户的会话。
密码学:使用加密算法保持数据安全,同时仍然易于使用。
还有其他功能可以在不同的应用程序环境中支持和强化这些问题,尤其是:
Web支持:Shiro的Web支持API可帮助轻松保护Web应用程序。
缓存: 缓存是Apache Shiro API中的第一层公民,可确保安全操作保持快速高效。
并发: Apache Shiro支持具有并发功能的多线程应用程序。
测试: 存在测试支持以帮助您编写单元和集成测试,并确保您的代码按预期受到保护。
“运行方式”: 允许用户假定其他用户的身份(如果允许)的功能,有时在管理方案中很有用。
“记住我”: 记住用户在会话中的身份,因此他们只需要在必要时登录。
二、架构分析
1.Shiro的架构有3个主要概念:和Subject,SecurityManager和Realms
- Subject
官网的描述为当前的用户、第三方服务等,其实就是与集成了shiro的系统交互的访客,抽象为Subject。
Subject实例必须绑定一个SecurityManager - SecurityManager
SecurityManager是Shiro架构的核心,充当一种“伞形”对象,协调其内部安全组件,共同形成对象图。 我们只需要对其进行相应的配置即可
当我们与Subject交互时,实际上工作的是幕后的SecurityManager,它可以完成任何Subject安全操作的繁重任务。 - Realms
Realms充当Shiro与应用程序安全数据之间的“桥梁”或“连接器”。当实际与安全相关数据(如用户帐户)进行交互以执行身份验证(登录)和授权(访问控制)时,Shiro会从为应用程序配置的一个或多个领域中查找许多这些内容。
从这个意义上讲,Realm本质上是一个特定于安全性的DAO:它封装了数据源的连接细节,并根据需要使相关数据可用于Shiro。配置Shiro时,必须至少指定一个Realm用于身份验证和/或授权。所述SecurityManager
可与多个境界被配置,但至少有一个是必需的。
Shiro提供了开箱即用的Realms,可以连接到许多安全数据源(也称为目录),如LDAP,关系数据库(JDBC),文本配置源(如INI和属性文件等)。如果默认域不符合您的需要,您可以插入自己的Realm实现来表示自定义数据源。
与其他内部组件一样,ShiroSecurityManager
管理如何使用Realms获取要表示为Subject
实例的安全性和身份数据。
2.详细架构
Subject(
org.apache.shiro.subject.Subject
)
当前与软件交互的实体(用户,第三方服务,cron作业等)的特定于安全性的“视图”。SecurityManager(org.apache.shiro.mgt.SecurityManager)
如上所述,这SecurityManager
是Shiro建筑的核心。它主要是一个“伞形”对象,协调其托管组件,以确保它们一起平稳运行。它还管理Shiro对每个应用程序用户的视图,因此它知道如何对每个用户执行安全操作。认证器(org.apache.shiro.authc.Authenticator)
的Authenticator
是,负责执行和反应以验证(注册)用户企图的组件。当用户尝试登录时,该逻辑由执行Authenticator
。该Authenticator
知道如何与一个或多个协调Realms
存储有关用户/帐户信息。从这些数据中获取的数据Realms
用于验证用户的身份,以保证用户确实是他们所说的人。AuthenticationStrategy(org.apache.shiro.authc.pam.AuthenticationStrategy)
如果Realm
配置了多个,AuthenticationStrategy
则将协调领域以确定身份验证尝试成功或失败的条件(例如,如果一个领域成功但其他领域失败尝试是否成功?必须所有领域成功吗?只有第一个?)。Authorizer(org.apache.shiro.authz.Authorizer)
的Authorizer
是部件负责确定用户在该应用程序的访问控制。这种机制最终会说明是否允许用户做某事。与此类似Authenticator
,它Authorizer
也知道如何协调多个后端数据源以访问角色和权限信息。在Authorizer
使用该信息来确定到底是否允许用户执行特定的操作。SessionManager(org.apache.shiro.session.mgt.SessionManager)
将SessionManager
知道如何创建和管理用户Session
生命周期,提供在所有环境中的用户强大的会话体验。这是安全框架领域的一项独特功能 - 即使没有可用的Web / Servlet或EJB容器,Shiro也能够在任何环境中本地管理用户Sessions。默认情况下,Shiro将使用现有的会话机制(例如Servlet容器),但如果没有,例如在独立应用程序或非Web环境中,它将使用其内置的企业会话管理提供相同的编程经验。的SessionDAO
存在允许任何数据源被用来坚持的会议。SessionDAO(org.apache.shiro.session.mgt.eis.SessionDAO)
的SessionDAO
执行Session
代表的持久性(CRUD)操作SessionManager
。这允许将任何数据存储插入会话管理基础结构。CacheManager(org.apache.shiro.cache.CacheManager)
的CacheManager
创建和管理Cache
其他四郎组件使用实例的生命周期。由于Shiro可以访问许多后端数据源以进行身份验证,授权和会话管理,因此缓存一直是框架中的一流架构功能,可在使用这些数据源时提高性能。任何现代开源和/或企业缓存产品都可以插入Shiro,以提供快速有效的用户体验。Cryptography(org.apache.shiro.crypto.*)
密码学是企业安全框架的自然补充。Shiro的crypto
软件包包含易于使用和理解的密码密码,哈希(aka摘要)和不同编解码器实现的表示。该软件包中的所有类都经过精心设计,易于使用且易于理解。使用Java本机加密支持的任何人都知道它可能是一个具有挑战性的驯服动物。Shiro的加密API简化了复杂的Java机制,使密码学易于用于普通的凡人。Realm(org.apache.shiro.realm.Realm)
如上所述,Realms充当Shiro与应用程序安全数据之间的“桥接”或“连接器”。当实际与安全相关数据(如用户帐户)进行交互以执行身份验证(登录)和授权(访问控制)时,Shiro会从为应用程序配置的一个或多个领域中查找许多这些内容。您可以根据Realms
需要配置任意数量(通常每个数据源一个),Shiro将根据需要进行身份验证和授权协调。
总结:Subject相当于shiro的门面(前台),负责对外交互,实际的验证授权等需要由SecurityManager决定,而SecurityManager做出决定需要经过数据验证,那么数据由Realm来提供,到此就把shiro的框架流程串起来了。
二、shiro整合spring
由于我们在开发中,基本上都是结合spring使用shiro,所以这里略过了官网的入门案例
Shiro一直支持Spring Web应用程序。在Web应用程序中,所有可通过Shiro访问的Web请求都必须通过主Shiro过滤器。此过滤器本身非常强大,允许基于任何URL路径表达式执行临时自定义过滤器链。
以下是如何在基于Spring的Web应用程序中配置Shiro:
1、采用xml方式配置(不推荐使用)
在web.xml中配置shiro过滤器
<!-- 过滤器的名称和你在applicationContext.xml中配置的bean注入必须一致 -->
<!--见applicationContext.xml中 <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
...
<!--在所有其他过滤器之前定义此过滤器,/ *捕获所有请求,确保过滤了您希望Shiro访问的任何请求。 -->
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
在applicationContext.xml中配置
<!-- id必须和web.xml中的filter名称匹配-->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<!-- override these for application-specific URLs if you like:
<!--指定跳转到的登录页面-->
<property name="loginUrl" value="/login.jsp"/>
<!--指定跳转到的成功页面-->
<property name="successUrl" value="/home.jsp"/>
<!--指定跳转到的无权限页面-->
<property name="unauthorizedUrl" value="/unauthorized.jsp"/> -->
<!-- The 'filters' property is not necessary since any declared javax.servlet.Filter bean -->
<!-- defined will be automatically acquired and available via its beanName in chain -->
<!-- definitions, but you can perform instance overrides or name aliases here if you like: -->
<!-- <property name="filters">
<util:map>
<entry key="anAlias" value-ref="someFilter"/>
</util:map>
</property> -->
<!--根据需要添加过滤器到过滤器链中-->
<property name="filterChainDefinitions">
<value>
# some example chain definitions:
/admin/** = authc, roles[admin]
/docs/** = authc, perms[document:read]
/** = authc
# more URL-to-FilterChain definitions here
</value>
</property>
</bean>
<!-- Define any javax.servlet.Filter beans you want anywhere in this application context. -->
<!-- They will automatically be acquired by the 'shiroFilter' bean above and made available -->
<!-- to the 'filterChainDefinitions' property. Or you can manually/explicitly add them -->
<!-- to the shiroFilter's 'filters' Map if desired. See its JavaDoc for more details. -->
<!--除了shiro提供的默认过滤器,也可以自定义过滤器-->
<bean id="someFilter" class="..."/>
<bean id="anotherFilter" class="..."> ... </bean>
...
<!--配置securityManager-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!-- Single realm app. If you have multiple realms, use the 'realms' property instead. -->
<property name="realm" ref="myRealm"/>
<!-- By default the servlet container sessions will be used. Uncomment this line
to use shiro's native sessions (see the JavaDoc for more): -->
<!-- <property name="sessionMode" value="native"/> -->
</bean>
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<!-- Define the Shiro Realm implementation you want to use to connect to your back-end -->
<!--配置自定义的realm,用户用户信息的比对-->
<!-- security datasource: -->
<bean id="myRealm" class="...">
...
</bean>
开启shiro注解
在请求接口中,也许我们会通过shiro的注解进行安全认证,(例如@RequiresRoles, @RequiresPermissions等等)
方法很简单,我们只需要在applicationContext.xml中添加如下配置,但注意,此时必须保证lifecycleBeanPostProcessor进行了配置
<!-- Enable Shiro Annotations for Spring-configured beans. Only run after -->
<!-- the lifecycleBeanProcessor has run: -->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
2.采用注解的方式进行配置(推荐使用)
shiro的配置
2.1 首先自定义Realm
/**
* 认证
*
*/
@Component
public class MyRealm extends AuthorizingRealm {
/**
* 授权(验证权限时调用)
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
User user = (User)principals.getPrimaryPrincipal();
Long userId = user.getUserId();
//用户权限列表
Set<String> permsSet = shiroService.getUserPermissions(userId);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setStringPermissions(permsSet);
return info;
}
/**
* 认证(登录时调用)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken)token;
String username = usernamePasswordToken.getUsername();
char[] password = usernamePasswordToken.getPassword();
//根据用户名和密码去验证用户
...
//验证通过后
User user = getUser();
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password,username);
return info;
}
}
2.2 shiro的配置类
/**
* Shiro配置
*
*/
@Configuration
public class ShiroConfig {
@Bean("securityManager")
public SecurityManager securityManager(MyRealm myRealm, SessionManager sessionManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myRealm);
securityManager.setSessionManager(sessionManager);
return securityManager;
}
@Bean("shiroFilter")
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
//放行一些不用权限验证的路径
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/webjars/**", "anon");
filterMap.put("/druid/**", "anon");
filterMap.put("/api/**", "anon");
filterMap.put("/sys/login", "anon");
filterMap.put("/**/*.css", "anon");
filterMap.put("/**/*.js", "anon");
filterMap.put("/**/*.html", "anon");
filterMap.put("/img/**", "anon");
filterMap.put("/fonts/**", "anon");
filterMap.put("/plugins/**", "anon");
filterMap.put("/swagger/**", "anon");
filterMap.put("/favicon.ico", "anon");
filterMap.put("/captcha.jpg", "anon");
filterMap.put("/", "anon");
filterMap.put("/**", "oauth2");
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
@Bean("lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
配置过滤器,将上面写的过滤器加入到容器中
package cn.environmental.config;
import cn.expand.filter.CorsFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.DelegatingFilterProxy;
import cn.environmental.common.xss.XssFilter;
import javax.servlet.DispatcherType;
/**
* Filter配置
*
*/
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean shiroFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
//注意shiroFilter和shiroConfig中匹配
registration.setFilter(new DelegatingFilterProxy("shiroFilter"));
//该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理
registration.addInitParameter("targetFilterLifecycle", "true");
registration.setEnabled(true);
registration.setOrder(Integer.MAX_VALUE - 1);
registration.addUrlPatterns("/*");
return registration;
}
}
三、Shiro整合JWT
JWT的介绍不在这里进行说明了,可以自己查阅相关资料
引入JWT后的过程如下:
3.1 在用户登录的成功的时候为其生成token,并返回,用户访问其他接口时需要携带token!
①引入jjwt依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
②生成token
public class JwtTest {
/****
* 创建Jwt令牌
*/
@Test
public void testCreateJwt(){
JwtBuilder builder= Jwts.builder()
.setId("888") //设置唯一编号
.setSubject("小白") //设置主题 可以是JSON数据
//.addClaims(xxx) 自定义载荷信息
.setIssuedAt(new Date()) //设置签发日期
//.setExpiration(date)//用于设置过期时间 ,参数为Date类型数据
.signWith(SignatureAlgorithm.HS256,"秘钥");//设置签名 使用HS256算法,并设置SecretKey(字符串)
//构建 并返回一个字符串
System.out.println( builder.compact() );
//eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjIyODd9.RBLpZ79USMplQyfJCZFD2muHV_KLks7M1ZsjTu6Aez4
}
}
③解析token
/***
* 解析Jwt令牌数据
*/
@Test
public void testParseJwt(){
String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjI5MjUsImV4cCI6MTU2MjA2MjkyNX0._vs4METaPkCza52LuN0-2NGGWIIO7v51xt40DHY1U1Q";
Claims claims = Jwts.parser().
setSigningKey("秘钥").
parseClaimsJws(compactJwt).
getBody();
System.out.println(claims);
}
3.2 引入token后需要在上面的shiroFilter中加入一个自定义的过滤器来专门验证token
/**
* token过滤器
*/
public class OAuth2Filter extends AuthenticatingFilter {
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
//获取请求token
String token = getRequestToken((HttpServletRequest) request);
if(StringUtils.isBlank(token)){
return null;
}
return new OAuth2Token(token);
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if(((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())){
return true;
}
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//获取请求token,如果token不存在,直接返回401
String token = getRequestToken((HttpServletRequest) request);
if(StringUtils.isBlank(token)){
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
String json = new Gson().toJson(R.error(HttpStatus.SC_UNAUTHORIZED, "invalid token"));
httpResponse.getWriter().print(json);
return false;
}
return executeLogin(request, response);
}
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setContentType("application/json;charset=utf-8");
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
try {
//处理登录失败的异常
Throwable throwable = e.getCause() == null ? e : e.getCause();
R r = R.error(HttpStatus.SC_UNAUTHORIZED, throwable.getMessage());
String json = new Gson().toJson(r);
httpResponse.getWriter().print(json);
} catch (IOException e1) {
}
return false;
}
/**
* 获取请求的token
*/
private String getRequestToken(HttpServletRequest httpRequest){
//从header中获取token
String token = httpRequest.getHeader("token");
//如果header中不存在token,则从参数中获取token
if(StringUtils.isBlank(token)){
token = httpRequest.getParameter("token");
}
return token;
}
}
public class OAuth2Token implements AuthenticationToken {
private String token;
public OAuth2Token(String token){
this.token = token;
}
@Override
public String getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
创建完验证token的过滤器后需要在shiroConfig中添加
@Configuration
public class ShiroConfig {
@Bean("securityManager")
public SecurityManager securityManager(OAuth2Realm oAuth2Realm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(oAuth2Realm);
securityManager.setRememberMeManager(null);
return securityManager;
}
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
//token过滤验证
Map<String, Filter> filters = new HashMap<>();
filters.put("oauth2", new OAuth2Filter());
shiroFilter.setFilters(filters);
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/webjars/**", "anon");
filterMap.put("/druid/**", "anon");
filterMap.put("/app/**", "anon");
filterMap.put("/sys/login", "anon");
filterMap.put("/swagger/**", "anon");
filterMap.put("/v2/api-docs", "anon");
filterMap.put("/swagger-ui.html", "anon");
filterMap.put("/swagger-resources/**", "anon");
filterMap.put("/captcha.jpg", "anon");
filterMap.put("/aaa.txt", "anon");
filterMap.put("/**", "oauth2");
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
@Bean("lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
token的过滤器的优先级比较高,这时候需要验证权限的接口就会先判断token是否有效了,需要将上面的Myrealm的认证方法改一下
/**
* 认证(登录时调用)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String accessToken = (String) token.getPrincipal();
//验证token的代码(略)
...
//token失效提醒用户(略)
//return
//查询用户信息
User user = queryUser(user.getUserId());
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, accessToken, getName());
return info;
}