SpringBoot 整合 Shiro
2019-04-30, 明天是五一劳动节,准备在4月结束前把 Token 令牌登录(SpringBoot 整合 JWT )的项目接口与将文件管理系统的接口对接,发现学长给的这个用户认证登录的接口有些问题(后续再说),没能顺利的进行。
- 在学习 SSM 框架的时候,有接触到 Shiro ,其中的登录是通过 Token 完成的。这样我便开始 SpringBoot 整合 Shiro。
一、Shiro 的简介
Shiro 完整架构图
Shiro 是 Apache 下的一个开源项目(Apache Shiro),是一个易用与 Java 项目的安全框架,提供了 认证、授权、加密、会话管理,与 Spring Security 一样都是做了一个权限的安全框架。
Shiro 核心的三大组件
Shiro 的三大核心组件:
1、Subject 当前用户。
2、SecurityManage 管理所有的 Subject 。
3、Reamls 权限信息的验证 。
除了三大组件,还要认识的内容:
* authenticator:认证器,主体进行认证最终通过authenticator进行的。
* authorizer:授权器,主体进行授权最终通过authorizer进行的。
* sessionManager:web应用中一般是用web容器对session进行管理,shiro也提供一套 - session管理的方式。
* SessionDao: 通过SessionDao管理session数据,针对个性化的session数据存储需要使用sessionDao。
* cache Manager:缓存管理器,主要对session和授权数据进行缓存,比如将授权数据通过cacheManager进行缓存管理,和ehcache整合对缓存数据进行管理。
* realm:域,领域,相当于数据源,通过realm存取认证、授权相关数据。
* cryptography:密码管理,提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能。比如 md5散列算法。
Shiro 的实现要点:
1、实现 Reamls 的 Authentication(验证用户的身份) 与 Authorization(用户授权访问控制)。
2、Shiro 是通过 Filter(过滤器)来实现的,就好比 SpringMVC 使用 DispatchServlet 来做控制,就是多个过滤器按照顺序执行。Shiro 是通过 URL 匹配规则来进行过滤和权限校验的,所以我们需要定义一系列的 URL 规则和访问权限。
Shiro 默认的拦截器 | 拦截器描述 | 用例 | 过滤器的类 org.apache.shiro.web.filter |
---|---|---|---|
anon | 可匿名访问 | 注册等可以匿名访问。/admin/register/**=anon | .authc.AnonymousFilter |
authc | 认证成功后才能使用(Authorization 执行成功) | 设置需要认证权限的界面。/user/**=authc | .authc.FormAuthenticationFilter |
logout | 注销登录的时候,任何 Session 都失效,任何身份都将失效 | 用户退出。/logout=logout | .authc.LogoutFilter |
authcBasic | 表示需通过 HttpBasic验证 | /user/**=authBasic | .authc.BasicHttpAuthenticationFilter |
perms | 权限过滤器 | /admins/**=perms[user:add:*];/admins/user/**=perms[“user:add:*,user:modify:*”] | .authz.PermissionsAuthorizationFilter |
port | 端口过滤器,可以设置是否是指定端口如果不是跳转到登录页面 | /admins/**=port[8081] | .authz.PortFilter |
rest | http方法过滤器 | 可以指定如post不能进行访问等,/admins/user/**=rest[user:method],其中method 为 post、get等 | .authz.HttpMethodPermissionFilter |
roles | 角色过滤器,判断当前用户是否指定角色。当有多个参数时,每个参数通过才算通过,相当于 hasAllRoles() 方法 | /admins/**=roles[“admin,guest”] | .authz.RolesAuthorizationFilter |
ssl | 请求需要通过ssl,如果不是跳转登录页 | 暂无 | .authz.SslFilter |
user | 如果访问一个已知用户,比如记住我功能,走这个过滤器 | 暂无 | .authc.UserFilter |
noSessionCreation | 阻止在请求期间创建新的会话。以保证无状态的体验 | 暂无 | .session.NoSessionCreationFilter |
3、Shiro 通过提供的会话管理可以获取 Session 中的信息,同样可以使用 CacheManage 来管理。
4、Shiro 的认证流程:
(1)、在登录页面输入账号密码,传递给 Controller,在 Controller 中通过 Security.getSubject() 获取当前的 Subject 。
(2)、通过 Subject 的 isAuthenticated() ,验证当前用户是否已经被认证。
(3)、如果没有被认证,则开始认证。
(4)、将从前台传来的账号密码封装到一个 UsernamePasswordToken 对象中。
(5)、调用当前 Subject 的 login() 方法。这会把 token 作为参数传递到自定义的 Realm 的 doGetAuthenticationInfo()方法中。
(6)、在 doGetAuthenticationInfo() 方法中,首先将 AuthenticationToken 转换为 UsernamePasswordToken 对象,然后调用 Service 层,根据 UsernamePasswordToken 中的用户名到数据库中去查询密码。
(7)、由 Shiro 完成密码的比对,密码的比对是通过 AuthenticatingRealm 的 credentialsMatcher 属性来进行比对的。
5、Shiro 的授权流程。
(1)、构造 SecurityManager 环境后,对 subject 进行授权,调用方法 isPermitted(”permission串”)
(2)、SecurityManager 执行授权,通过 ModularRealmAuthorizer 执行授权
(3)、ModularRealmAuthorizer 执行 realm(自定义的CustomRealm)从数据库查询权限数据
调用realm的授权方法:doGetAuthorizationInfo()
(4)、realm 从数据库查询权限数据,返回ModularRealmAuthorizer
(5)、ModularRealmAuthorizer 调用 PermissionResolver 进行权限串比对
(6)、如果比对后,isPermitted 中”permission串”在 realm 查询到权限数据中,说明用户访问 permission 串有权限,否则没有权限,抛出异常。
二、Shiro 的实现。
独立应用程序
我是直接开发的 Web ,这里是官网标出来,我大概理解了一下。如有什么不对的地方,请见谅。
-
自定义的 Realm 必须继承 AuthorizingRealm,并实现两个 Abstract 方法 doGetAuthorizationInfo 和 doGetAuthenticationInfo。
-
页面的登录表单(Login Form)按照官方文档的写法,表单元素的名字都和文档一样(即:username、password 、rememberMe),表单登录(Login Action)只需要完成验证失败的跳转。验证成功变跳转至 successUrl。
默认的 FormAuthenticationFilter 会找这三个requestparameters:username、password 、rememberMe。
设置FormAuthenticationFilter的这几个参数。
authc.loginUrl = /login.html
authc.usernameParam = adminName
authc.passwordParam = adminPass
authc.rememberMeParam = addCookie
2.1、SSM 框架配置文件的方式 集成 Apache Shiro 。
SSM 框架 Shiro 的实现,请参考:https://blog.csdn.net/Roobert_Chao/article/details/89971383
2.2、Spring Boot 集成 Apache Shiro 。
Spring Boot 集成 Apache Shiro,请参考: https://blog.csdn.net/Roobert_Chao/article/details/89971828
Spring Boot 集成 Apache Shiro 并添加 Cache ,请参考:https://blog.csdn.net/Roobert_Chao/article/details/90046565
三、Shiro 的方法总结。
3.1、Shiro 支持三种方式的授权。
第一种,subject.hasRole(“admin”);主体中有admin权限?
Subject subject = SecurityUtils.getSubject();
if(subject.hasRole(“admin”)) {
// 执行有权限的操作
} else {
// 执行无权限的操作
}
第二种,<shiro:hasRole name=’’ > JSP页面的标签?
# 刚开始接触 Web 的时候,我只会写 JSP 页面。
<shiro:hasRole name="admin">
<!-- 有权限 -->
</shiro:hasRole>
第三种,学会使用注解之后进常用的。
# 注解式:通过在执行的Java方法上放置相应
@RequiresRoles("admin")
public void hello() {
// 执行有权限的操作
}
3.2、@RequiresRoles 和 @RequiresPermissions 注解不生效。
- 上边,刚刚说过授权的第三种方式使用注解,但是注解必须必须要使用相应的bean才能生效。
- 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),
需借助SpringAOP扫描使用Shiro注解的类
,并在必要时进行安全逻辑验证,配置以下两个bean(DefaultAdvisorAutoProxyCreator
和AuthorizationAttributeSourceAdvisor
)。
# Shiro 权限注解要生效,必须配置 Spring AOP 通过设置 Shiro 的 SecurityManager 进行权限验证。
================= 注解的方式加入Bean ======================
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true); // 开启代理
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager()); // 配置安全管理器。
return authorizationAttributeSourceAdvisor;
}
================= 配置文件的方式加入 Bean ==================
<!-- 开启Shiro Spring AOP权限注解的支持;<aop:config proxy-target-class="true">表示代理类。 -->
<aop:config proxy-target-class="true">
<!-- 生命周期 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<!-- 注解方式出现异常 -->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<prop key="org.apache.shiro.authz.UnauthorizedException">shiro-test/refuse</prop>
</props>
</property>
</bean>
<!-- 认证用户的注解管理 -->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor">
<property name="proxyTargetClass" value="true"/>
<bean>
<!-- 授权用户的注解管理 -->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
### 拦截异常 。
@ExceptionHandler({UnauthorizedException.class})
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ModelAndView processUnauthenticatedException(NativeWebRequest request, UnauthorizedException e) {
ModelAndView mv = new ModelAndView();
mv.addObject("exception", e);
mv.setViewName("unauthorized");
return mv;
}
3.3、Shiro 的注解。
- 注解既可以用在 controller 中,也可以用在 service 中使用。
建议将shiro注解放在 controller 中
,因为如果 service 层使用了spring的事物注解,那么 shiro 注解将无效。
@RequiresAuthentication
表示当前 Subject 已经通过 login 进行了身份验证;即 Subject.isAuthenticated() 返回true。
@RequiresUser
表示当前 Subject 已经身份验证或者通过记住我登录的。
@RequiresGuest
表示当前 Subject 没有身份验证或通过记住我登录过,即是游客身份。
@RequiresRoles(value={“admin”, “user”}, logical= Logical.AND)
@RequiresRoles(value={“admin”})
@RequiresRoles({“admin“})
表示当前 Subject 需要角色 admin 和 user。
@RequiresPermissions (value={“user:create”, “user:delete”}, logical= Logical.OR)
表示当前 Subject 需要权限 user:create 或 user:delete。
3.4、unauthorizedUrl 无效。
- <property name=“unauthorizedUrl” value="/unauthorized.html"/>
- shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
我们在测试之前,发现都起作用,但是在 SSM 框架整合之后,抛出异常,并没有跳转到无权限页面。原因是因为 页面的跳转交给springMVC来控制,权限认证不通过, SpringMVC 抛出了异常。
【方法一:注册 Bean SimpleMappingExceptionResolver 设置异常对应的 url,url 需要在 Controller 中指定
】
@Bean
public SimpleMappingExceptionResolver simpleMappingExceptionResolver() {
SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
Properties properties = new Properties();
/*未授权处理页*/
properties.setProperty("org.apache.shiro.authz.UnauthorizedException", "/unauthor.html");
/*身份没有验证*/
properties.setProperty("org.apache.shiro.authz.UnauthenticatedException", "/login.html");
resolver.setExceptionMappings(properties);
return resolver;
}
- 亲测可以。
【方法二:自定义异常类 Reslover 捕捉异常,如果异常为无权限异常就手动就是转发到无权页面。
】
1、首先设置异常的捕获类,
public class MyExceptionResolver implements HandlerExceptionResolver{
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception e) {
// 如果是shiro无权操作,因为shiro 在操作auno 等一部分不进行转发至无权限url
if(e instanceof UnauthorizedException){
ModelAndView mv = new ModelAndView("unauthor");
return mv;
}
e.printStackTrace();
ModelAndView mv = new ModelAndView("unauthor");
mv.addObject("exception", e.toString().replaceAll("\n", "<br/>"));
return mv;
}
}
2. 自定义异常处理
<bean id="exceptionResolver" class="cn.chao.formyself.resolver.MyExceptionResolver"></bean>
3.5、AJAX 请求,提示无角色无权限。
在上面的异常处理中,我们将判断一个请求,是否拥有角色的权限,无没有权限的话,则跳转到了无权限的页面中,那么我们要使用 AJAX 请求,返回的 error:function() 中应该是一个没有权限的提示信息。
【方法一:仍然使用第一种抛出异常的 Bean 。】
@Bean
public SimpleMappingExceptionResolver simpleMappingExceptionResolver() {
SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
Properties properties = new Properties();
/*未授权处理页*/
properties.setProperty("org.apache.shiro.authz.UnauthorizedException", "/访问方法判断是否是 AJAX 处理");
/*身份没有验证*/
properties.setProperty("org.apache.shiro.authz.UnauthenticatedException", "/login.html");
resolver.setExceptionMappings(properties);
return resolver;
}
【方法二:配置文件结合 Controller 层的代码判断】
配置异常的拦截器
<!-- shiro 为集成 springmvc 拦截异常 -->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings" >
<props>
<!-- 这里可以配置 N 多个 错误异常转发 -->
<prop key="org.apache.shiro.authz.AuthorizationException">redirect:/401</prop>
</property>
</bean>
异常拦截器将异常重定向到了 RequestMapping 401 的 controller 中
@RequestMapping("/401")
public String authorizationException(ModelMap modelMap,HttpServletRequest request){
String requestType = request.getHeader("x-Requested-With");
// AJAX 请求
if(requestType != null && requestType.equals("XMLHttpRequest")){
return "redirect:ajax401"; // 我在这里抛出的是权限的异常,判断为 AJAX 的请求方式,这里调用 RequestMapping("ajax401") 的方法,返回String 类型字符串,提示没有数据。
}else{
return "redirect:syn401"; // 方法体直接放回 无权限页面。
}
}