-
Shiro安全框架简介
-
Shiro概述
Shiro是apache旗下一个开源安全框架(http://shiro.apache.org/),它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。使用shiro就可以非常快速的完成认证、授权等功能的开发,降低系统成本。
用户在进行资源访问时,要求系统要对用户进行权限控制,其具体流程如图-1所示:
-
Shiro概要架构
在概念层面,Shiro 架构包含三个主要的理念,如图-2所示:
其中:
-
Subject :主体对象,负责提交用户认证和授权信息。
-
SecurityManager:安全管理器,负责认证,授权等业务实现。
-
Realm:领域对象,负责从数据层获取业务数据。
-
Shiro详细架构
Shiro框架进行权限管理时,要涉及到的一些核心对象,主要包括:认证管理对象,授权管理对象,会话管理对象,缓存管理对象,加密管理对象以及Realm管理对象(领域对象:负责处理认证和授权领域的数据访问题)等,其具体架构如图-3所示:
其中:
-
Subject(主体):与软件交互的一个特定的实体(用户、第三方服务等)。
-
SecurityManager(安全管理器) :Shiro 的核心,用来协调管理组件工作。
-
Authenticator(认证管理器):负责执行认证操作。
-
Authorizer(授权管理器):负责授权检测。
-
SessionManager(会话管理):负责创建并管理用户 Session 生命周期,提供一个强有力的 Session 体验。
-
SessionDAO:代表 SessionManager 执行 Session 持久(CRUD)动作,它允许任何存储的数据挂接到 session 管理基础上。
-
CacheManager(缓存管理器):提供创建缓存实例和管理缓存生命周期的功能。
-
Cryptography(加密管理器):提供了加密方式的设计及管理。
-
Realms(领域对象):是shiro和你的应用程序安全数据之间的桥梁。
-
Shiro框架认证拦截实现(filter)
-
Shiro基本环境配置
-
添加shiro依赖
实用spring整合shiro时,需要在pom.xml中添加如下依赖:
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.5.2</version> </dependency>
-
Shiro核心对象配置
基于SpringBoot 实现的项目中,没有提供shiro的自动化配置,需要我们自己配置。
第一步:创建SpringShiroConfig类。关键代码如下:
package com.cy.pj.common.config; /**@Configuration 注解描述的类为一个配置对象, * 此对象也会交给spring管理 */ @Configuration public class SpringShiroConfig { }
第二步:在Shiro配置类中添加SecurityManager配置(这里一定要使用org.apache.shiro.mgt.SecurityManager这个接口对象),关键代码如下:
@Bean public SecurityManager securityManager() { DefaultWebSecurityManager sManager= new DefaultWebSecurityManager(); return sManager; }
第三步: 在Shiro配置类中添加ShiroFilterFactoryBean对象的配置。通过此对象设置资源匿名访问、认证访问。关键代码如下
-
-
@Bean public ShiroFilterFactoryBean shiroFilterFactory ( SecurityManager securityManager,) { ShiroFilterFactoryBean sfBean= new ShiroFilterFactoryBean(); sfBean.setSecurityManager(securityManager); //定义map指定请求过滤规则(哪些资源允许匿名访问,哪些必须认证访问) LinkedHashMap<String,String> map= new LinkedHashMap<>(); //静态资源允许匿名访问:"anon" map.put("/bower_components/**","anon"); map.put("/build/**","anon"); map.put("/dist/**","anon"); map.put("/plugins/**","anon"); //除了匿名访问的资源,其它都要认证("authc")后访问 map.put("/**","authc"); sfBean.setFilterChainDefinitionMap(map); return sfBean; }
其配置过程中,对象关系如下图-4所示:
-
Shiro登陆页面呈现
-
服务端Controller实现
-
业务描述及设计实现
当服务端拦截到用户请求以后,判定此请求是否已经被认证,假如没有认证应该先跳转到登录页面。
-
关键代码分析及实现.
第一步:在PageController中添加一个呈现登录页面的方法,关键代码如下:
@RequestMapping("doLoginUI") public String doLoginUI(){ return "login"; }
第二步:修改SpringShiroConfig类中shiroFilterFactorybean的配置,添加登陆url的设置。关键代码见sfBean.setLoginUrl("/doLoginUI") 第7行 部分。
1 @Bean 2 public ShiroFilterFactoryBean shiroFilterFactory ( 3 @Autowired SecurityManager securityManager) { 4 ShiroFilterFactoryBean sfBean= 5 new ShiroFilterFactoryBean(); 6 sfBean.setSecurityManager(securityManager); 7 sfBean.setLoginUrl("/doLoginUI"); 8 //定义map指定请求过滤规则(哪些资源允许匿名访问, 9 哪些必须认证访问) 10 LinkedHashMap<String,String> map= 11 new LinkedHashMap<>(); 12 //静态资源允许匿名访问:"anon" 13 map.put("/bower_components/**","anon"); 14 map.put("/modules/**","anon"); 15 map.put("/dist/**","anon"); 16 map.put("/plugins/**","anon"); 17 //除了匿名访问的资源,其它都要认证("authc")后访问 18 map.put("/**","authc"); 19 sfBean.setFilterChainDefinitionMap(map); 20 return sfBean; 21 }
-
Shiro框架认证业务实现
-
认证流程分析
身份认证即判定用户是否是系统的合法用户,用户访问系统资源时的认证(对用户身份信息的认证)流程图-5所示:
步骤1:应用程序代码调用该Subject.login
方法,并传入AuthenticationToken
表示最终用户的主体和凭据的构造实例。
第2步:Subject
实例(通常是一个DelegatingSubject
(或子类))SecurityManager
通过调用来委托应用程序,实例securityManager.login(token)
从此处开始实际的身份验证工作。
步骤3:SecurityManager
作为基本的“伞”组件,接收令牌并Authenticator
通过调用来简单地委派给其内部实例authenticator.authenticate(token)
。这几乎总是一个ModularRealmAuthenticator
实例,它支持Realm
在身份验证期间协调一个或多个实例。在ModularRealmAuthenticator
本质上提供一个PAM为Apache四郎(其中每个样式的范例Realm
是在PAM术语一个“模块”)。
步骤4:如果Realm
为该应用程序配置了多个,则该ModularRealmAuthenticator
实例将Realm
使用其configureed 发起多身份验证尝试AuthenticationStrategy
。在Realms
调用进行身份验证之前,期间和之后,将调用,AuthenticationStrategy
以允许它对每个Realm的结果做出反应。我们将AuthenticationStrategies
尽快覆盖。
第5步:Realm
咨询每个配置,以查看是否supports
已提交AuthenticationToken
。如果是这样,支持的Realm getAuthenticationInfo
方法将与Submitted一起调用token
。该getAuthenticationInfo
方法有效地表示针对该特定对象的单个身份验证尝试Realm
。我们将Realm
很快介绍身份验证行为。
简单说就是
-
系统调用subject的login方法将用户信息提交给SecurityManager
-
SecurityManager将认证操作委托给认证器对象Authenticator
-
Authenticator将用户输入的身份信息传递给Realm。
-
Realm访问数据库获取用户信息然后对信息进行封装并返回。
-
Authenticator 对realm返回的信息进行身份认证。
-
认证服务端实现
-
核心业务分析
认证业务API处理流程分析,如图-6所示:
-
业务描述及设计实现。
在用户数据层对象SysUserDao中,按特定条件查询用户信息,并对其进行封装。
-
关键代码分析及实现。
在SysUserDao接口中,添加根据用户名获取用户对象的方法,关键代码如下:
SysUser findUserByUserName(String username)。
-
Mapper元素定义
-
业务描述及设计实现。
根据SysUserDao中定义的方法,在SysUserMapper文件中添加元素定义。
-
关键代码分析及实现。
基于用户名获取用户对象的方法,关键代码如下:
<select id="findUserByUserName" resultType="com.cy.pj.sys.entity.SysUser"> select * from sys_users where username=#{username} </select>
-
Service接口及实现
-
业务描述及设计实现。
本模块的业务在Realm类型的对象中进行实现,我们编写realm时,要继承
AuthorizingRealm并重写相关方法,完成认证及授权业务数据的获取及封装。
-
关键代码分析及实现。
第一步:定义ShiroUserRealm类,关键代码如下:
package com.cy.pj.sys.service.realm; @Service public class ShiroUserRealm extends AuthorizingRealm { @Autowired private SysUserDao sysUserDao; /** * 设置凭证匹配器(与用户添加操作使用相同的加密算法) */ @Override public void setCredentialsMatcher( CredentialsMatcher credentialsMatcher) { //构建凭证匹配对象 HashedCredentialsMatcher cMatcher= new HashedCredentialsMatcher(); //设置加密算法 cMatcher.setHashAlgorithmName("MD5"); //设置加密次数 cMatcher.setHashIterations(1); super.setCredentialsMatcher(cMatcher); } /** * 通过此方法完成认证数据的获取及封装,系统 * 底层会将认证数据传递认证管理器,由认证 * 管理器完成认证操作。 */ @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token) throws AuthenticationException { //1.获取用户名(用户页面输入) UsernamePasswordToken upToken= (UsernamePasswordToken)token; String username=upToken.getUsername(); //2.基于用户名查询用户信息 SysUser user= sysUserDao.findUserByUserName(username); //3.判定用户是否存在 if(user==null) throw new UnknownAccountException(); //4.判定用户是否已被禁用。 if(user.getValid()==0) throw new LockedAccountException(); //5.封装用户信息 ByteSource credentialsSalt= ByteSource.Util.bytes(user.getSalt()); //记住:构建什么对象要看方法的返回值 SimpleAuthenticationInfo info= new SimpleAuthenticationInfo( user,//principal (身份) user.getPassword(),//hashedCredentials credentialsSalt, //credentialsSalt getName());//realName //6.返回封装结果 return info;//返回值会传递给认证管理器(后续 //认证管理器会通过此信息完成认证操作) } .... }
第二步:对此realm,需要在SpringShiroConfig配置类中,注入给SecurityManager对象,修改securityManager方法,见黄色背景部分,例如:
1 @Bean 2 public SecurityManager securityManager(Realm realm) { 3 DefaultWebSecurityManager sManager= 4 new DefaultWebSecurityManager(); 5 sManager.setRealm(realm); 6 return sManager; 7 }
-
Controller 类实现
-
业务描述及设计实现。
在此对象中定义相关方法,处理客户端的登陆请求,例如获取用户名,密码等然后提交该shiro框架进行认证。
-
关键代码分析及实现。
第一步:在SysUserController中添加处理登陆的方法。关键代码如下:
1 @RequestMapping("doLogin") 2 public JsonResult doLogin(String username,String password){ 3 //1.获取Subject对象 4 Subject subject=SecurityUtils.getSubject(); 5 //2.通过Subject提交用户信息,交给shiro框架进行认证操作 6 //2.1对用户进行封装 7 UsernamePasswordToken token= 8 new UsernamePasswordToken( 9 username,//身份信息 10 password);//凭证信息 11 //2.2对用户信息进行身份认证 12 subject.login(token); 13 //分析: 14 //1)token会传给shiro的SecurityManager 15 //2)SecurityManager将token传递给认证管理器 16 //3)认证管理器会将token传递给realm 17 return new JsonResult("login ok"); 18 }
第二步:修改shiroFilterFactory的配置,对/user/doLogin这个路径进行匿名访问的配置,查看如下黄色标记部分的代码:
@Bean public ShiroFilterFactoryBean shiroFilterFactory ( SecurityManager securityManager) { ShiroFilterFactoryBean sfBean= new ShiroFilterFactoryBean(); sfBean.setSecurityManager(securityManager); //假如没有认证请求先访问此认证的url sfBean.setLoginUrl("/doLoginUI"); //定义map指定请求过滤规则(哪些资源允许匿名访问,哪些必须认证访问) LinkedHashMap<String,String> map= new LinkedHashMap<>(); //静态资源允许匿名访问:"anon" map.put("/bower_components/**","anon"); map.put("/build/**","anon"); map.put("/dist/**","anon"); map.put("/plugins/**","anon"); map.put("/user/doLogin","anon"); //authc表示,除了匿名访问的资源,其它都要认证("authc")后才能访问访问 map.put("/**","authc"); sfBean.setFilterChainDefinitionMap(map); return sfBean; }
第三步:当我们在执行登录操作时,为了提高用户体验,可对系统中的异常信息进行处理,例如,在统一异常处理类中添加如下方法:
@ExceptionHandler(ShiroException.class) @ResponseBody public JsonResult doHandleShiroException( ShiroException e) { JsonResult r=new JsonResult(); r.setState(0); if(e instanceof UnknownAccountException) { r.setMessage("账户不存在"); }else if(e instanceof LockedAccountException) { r.setMessage("账户已被禁用"); }else if(e instanceof IncorrectCredentialsException) { r.setMessage("密码不正确"); }else if(e instanceof AuthorizationException) { r.setMessage("没有此操作权限"); }else { r.setMessage("系统维护中"); } e.printStackTrace(); return r; }
-