# 权限
# Shiro
| 官方文档
shiro
在 Java 领域一般有 Spring Security、 Apache Shiro 等安全框架,但是由于 Spring Security 过于庞大和复杂,大多数公司会选择 Apache Shiro 来使用
Apache Shiro 是一个功能强大、灵活的,开源的安全框架。它可以干净利落地处理身份验证、授权、企业会话管理和加密。
Apache Shiro 的首要目标是易于使用和理解。安全通常很复杂,甚至让人感到很痛苦,但是 Shiro 却不是这样子的。一个好的安全框架应该屏蔽复杂性,向外暴露简单、直观的 API,来简化开发人员实现应用程序安全所花费的时间和精力。
# Shiro能做什么呢
- 验证用户身份
- 用户访问权限控制,比如:
- 1、判断用户是否分配了一定的安全角色。
- 2、判断用户是否被授予完成某个操作的权限
- 在非 Web 或 EJB 容器的环境下可以任意使用 Session API
- 可以响应认证、访问控制,或者 Session 生命周期中发生的事件
- 可将一个或以上用户安全数据源数据组合成一个复合的用户 “view”(视图)
- 支持单点登录(SSO)功能
- 支持提供“Remember Me”服务,获取用户关联信息而无需登录
- …
- 等等——都集成到一个有凝聚力的易于使用的 API。
Shiro 致力在所有应用环境下实现上述功能,小到命令行应用程序,大到企业应用中,而且不需要借助第三方框架、容器、应用服务器等。当然 Shiro 的目的是尽量的融入到这样的应用环境中去,但也可以在它们之外的任何环境下开箱即用。
# Shiro特性
Apache Shiro 是一个全面的、蕴含丰富功能的安全框架。下图为描述 Shiro 功能的框架图:


Authentication(认证), Authorization(授权), Session Management(会话管理), Cryptography(加密)被 Shiro 框架的开发团队称之为应用安全的四大基石。那么就让我们来看看它们吧:
- Authentication(认证):用户身份识别,通常被称为用户“登录”
- Authorization(授权):访问控制。比如某个用户是否具有某个操作的使用权限。
- Session Management(会话管理):特定于用户的会话管理,甚至在非web 或 EJB 应用程序。
- Cryptography(加密):在对数据源使用加密算法加密的同时,保证易于使用。
还有其他的功能来支持和加强这些不同应用环境下安全领域的关注点。特别是对以下的功能支持:
- Web支持:Shiro 提供的 Web 支持 api ,可以很轻松的保护 Web 应用程序的安全。
- 缓存:缓存是 Apache Shiro 保证安全操作快速、高效的重要手段。
- 并发:Apache Shiro 支持多线程应用程序的并发特性。
- 测试:支持单元测试和集成测试,确保代码和预想的一样安全。
- “Run As”:这个功能允许用户假设另一个用户的身份(在许可的前提下)。
- “Remember Me”:跨 session 记录用户的身份,只有在强制需要时才需要登录。
注意: Shiro 不会去维护用户、维护权限,这些需要我们自己去设计/提供,然后通过相应的接口注入给 Shiro
# 高级概述
在概念层,Shiro 架构包含三个主要的理念:Subject,SecurityManager和 Realm。下面的图展示了这些组件如何相互作用,我们将在下面依次对其进行描述。

- Subject:当前用户,Subject 可以是一个人,但也可以是第三方服务、守护进程帐户、时钟守护任务或者其它–当前和软件交互的任何事件。
- SecurityManager:管理所有Subject,SecurityManager 是 Shiro 架构的核心,配合内部安全组件共同组成安全伞。
- Realms:用于进行权限信息的验证,我们自己实现。Realm 本质上是一个特定的安全 DAO:它封装与数据源连接的细节,得到Shiro 所需的相关的数据。在
配置 Shiro 的时候,你必须指定至少一个Realm 来实现认证(authentication)和/或授权(authorization)。
我们需要实现Realms的Authentication 和 Authorization。其中 Authentication 是用来验证用户身份,Authorization 是授权访问控制,用于对用户进行的操作授权,证明该用户是否允许进行当前操作,如访问某个链接,某个资源文件等。
# 快速上手shiro
pom文件修改
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring-boot-starter -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.4.1</version>
</dependency>
2
3
4
5
6
https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
2
3
4
5
6
# RBAC
RBAC 是基于角色的访问控制(Role-Based Access Control )在 RBAC 中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便。
# Shiro配置
首先要配置的是 ShiroConfig 类,Apache Shiro 核心通过 Filter 来实现,就好像 SpringMvc 通过 DispachServlet 来主控制一样。 既然是使用 Filter 一般也就能猜到,是通过 URL 规则来进行过滤和权限校验,所以我们需要定义一系列关于 URL 的规则和访问权限。
@Configuration
@Slf4j
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
log.info("配置shiro");
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
// 拦截器.authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问
Map<String,String> filterChainMap = Maps.newLinkedHashMap();
// 配置不会被拦截的链接 顺序判断
filterChainMap.put("/static/**","anon");
// 配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
filterChainMap.put("/logout","logout");
// 过滤链定义,从上向下顺序执行,一般将/**放在最为下边
filterChainMap.put("/**","authc");
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilter.setLoginUrl("/login");
// 登录成功后要跳转的链接
shiroFilter.setSuccessUrl("/index");
// 未授权界面;
shiroFilter.setUnauthorizedUrl("/403");
shiroFilter.setFilterChainDefinitionMap(filterChainMap);
return shiroFilter;
}
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(null);
return defaultWebSecurityManager;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
The default Filter instances available automatically are defined by the DefaultFilter enum and the enum’s name field is the name available for configuration. They are:
| Filter Name | Class |
|---|---|
| anon | org.apache.shiro.web.filter.authc.AnonymousFilter所有 url 都都可以匿名访问 |
| authc | org.apache.shiro.web.filter.authc.FormAuthenticationFilter需要认证才能进行访问 |
| authcBasic | org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter |
| logout | org.apache.shiro.web.filter.authc.LogoutFilter |
| noSessionCreation | org.apache.shiro.web.filter.session.NoSessionCreationFilter |
| perms | org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter |
| port | org.apache.shiro.web.filter.authz.PortFilter |
| rest | org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter |
| roles | org.apache.shiro.web.filter.authz.RolesAuthorizationFilter |
| ssl | org.apache.shiro.web.filter.authz.SslFilter |
| user | org.apache.shiro.web.filter.authc.UserFilter配置记住我或认证通过可以访问 |
Filter Chain 定义说明:
- 一个URL可以配置多个 Filter,使用逗号分隔
- 当设置多个过滤器时,全部验证通过,才视为通过
- 部分过滤器可指定参数,如 perms,roles
# 登录认证实现
在认证、授权内部实现机制中都有提到,最终处理都将交给Realm进行处理。因为在 Shiro 中,最终是通过 Realm 来获取应用程序中的用户、角色及权限信息的。通常情况下,在 Realm 中会直接从我们的数据源中获取 Shiro 需要的验证信息。可以说,Realm 是专用于安全框架的 DAO. Shiro 的认证过程最终会交由 Realm 执行,这时会调用 Realm 的getAuthenticationInfo(token)方法。
该方法主要执行以下操作:
- 检查提交的进行认证的令牌信息
- 根据令牌信息从数据源(通常为数据库)中获取用户信息
- 对用户信息进行匹配验证。
- 验证通过将返回一个封装了用户信息的AuthenticationInfo实例。
- 验证失败则抛出AuthenticationException异常信息。
而在我们的应用程序中要做的就是自定义一个 Realm 类,继承AuthorizingRealm 抽象类,重载 doGetAuthenticationInfo(),重写获取用户信息的方法。
@Slf4j
public class MyShiroRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
String userCode = usernamePasswordToken.getUsername();
UserEntity userEntity = this.userService.findByCode(userCode);
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userEntity,
userEntity.getPassword(),getName());
authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(userEntity.getSalt()));
return authenticationInfo;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 认证流程说明
用户访问/user/login 路径,生成 UsernamePasswordToken, 通过SecurityUtils.getSubject()获取Subject(currentUser),调用 login 方法进行验证,
使用顺序:Shiro注解是存在顺序的,当多个注解在一个方法上的时候,会逐个检查,知道全部通过为止,默认拦截顺序是:
RequiresRoles->RequiresPermissions->RequiresAuthentication-> RequiresUser->RequiresGuest
# 错误说明集锦
在引用shiro-spring-boot-starter之后出现以下问题
Description:
Parameter 0 of method authorizationAttributeSourceAdvisor in org.apache.shiro.spring.boot.autoconfigure.ShiroAnnotationProcessorAutoConfiguration required a bean named 'authenticator' that could not be found.
Action:
Consider defining a bean named 'authenticator' in your configuration.
2
3
4
5
6
7
8
修改ShiroConfig.java中的bean的返回类型为DefaultWebSecurityManager(而不是SecurityManager)
@Bean
public DefaultWebSecurityManager webSecurityManager(){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(myShiroRealm());
return defaultWebSecurityManager;
}
2
3
4
5
6
当然也可以选择不引入shiro-spring-boot-starter,而是引入shiro-spring,那么那个bean就可以返回org.apache.shiro.mgt.SecurityManager
# Acegi
由于Spring在越来越多的项目中的应用,因此基于Spring应用的安全控制系统的研究就显得非常重要。Acegi提供了对Spring应用安全的支持,然而 Acegi本身提供的实例并不能满足大规模的复杂的权限需求,
# SpringBoot中如何灵活的实现接口数据的加解密功能
# 加密方案介绍
对接口的加密解密操作主要有下面两种方式:
1.自定义消息转换器
优势:仅需实现接口,配置简单。
劣势:仅能对同一类型的MediaType进行加解密操作,不灵活。2.使用spring提供的接口
RequestBodyAdvice和ResponseBodyAdvice
优势:可以按照请求的Referrer、Header或url进行判断,按照特定需要进行加密解密。
比如在一个项目升级的时候,新开发功能的接口需要加解密,老功能模块走之前的逻辑不加密,这时候就只能选择上面的第二种方式了,下面主要介绍下第二种方式加密、解密的过程
# 实现原理
RequestBodyAdvice可以理解为在@RequestBody之前需要进行的操作,
ResponseBodyAdvice可以理解为在@ResponseBody之后进行的操作,
所以当接口需要加解密时,在使用@RequestBody接收前台参数之前可以先在RequestBodyAdvice的实现类中进行参数的解密,当操作结束需要返回数据时,可以在@ResponseBody之后进入ResponseBodyAdvice的实现类中进行参数的加密。
自定义解密前端传送的参数
/**
* <p>Title: DecryptRequestBodyAdvice</p>
* <p>Description: </p>
*
* @author huting
* @date 2019/11/12 12:57
*/
@Slf4j
@Component
public class DecryptRequestBodyAdvice implements RequestBodyAdvice {
@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType
, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
return inputMessage;
}
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType
, Class<? extends HttpMessageConverter<?>> converterType) {
String dealData = null;
try {
//解密操作
Map<String,String> dataMap = (Map)body;
String srcData = dataMap.get("data");
dealData = DesUtil.decrypt(srcData);
} catch (Exception e) {
log.error("异常!", e);
}
return dealData;
}
@Override
public Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType
, Class<? extends HttpMessageConverter<?>> converterType) {
log.info("Empty Body");
return body;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
自定义加密返回到前端的数据
/**
* <p>Title: EncryResponseBodyAdvice</p>
* <p>Description: </p>
*
* @author huting
* @date 2019/11/12 13:03
*/
@Component
@Slf4j
public class EncryResponseBodyAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType
, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
//通过 ServerHttpRequest的实现类ServletServerHttpRequest 获得HttpServletRequest
ServletServerHttpRequest sshr = (ServletServerHttpRequest) request;
//此处获取到request 是为了取到在拦截器里面设置的一个对象 是我项目需要,可以忽略
HttpServletRequest httpRequest = sshr.getServletRequest();
String returnStr = "";
try {
//添加encry header,告诉前端数据已加密
response.getHeaders().add("encry", "true");
String srcData = JSON.toJSONString(body);
//加密
returnStr = DesUtil.encrypt(srcData);
log.info("接口={},原始数据={},加密后数据={}", httpRequest.getRequestURI(), srcData, returnStr);
} catch (Exception e) {
log.error("异常!", e);
}
return returnStr;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36