1、Shiro 框架简介
Shiro 概述 Shiro 是Apache公司推出一个权限管理框架,其内部封装了项目中认证,授权,加密,会话等逻辑操作,通过Shiro框架可以简化我们项目权限控制逻辑的代码的编写。其认证和授权业务分析,如图所示: Shiro 框架概要架构 Shiro 框架中主要通过Subject,SecurityManager,Realm对象完整认证和授权业务,其简要架构如下: 其中: Subject 此对象负责提交用户身份、权限等信息 SecurityManager 负责完成认证、授权等核心业务 Realm 负责通过数据逻辑对象获取数据库或文件中的数据。
Shiro 框架详细架构分析 Shiro 框架进行权限管理时,要涉及到的一些核心对象,主要包括:认证管理对象,授权管理对象,会话管理对象,缓存管理对象,加密管理对象以及 Realm 管理对象(领域对象:负责处理认证和授权领域的数据访问题)等,其具体架构如图- 所示: 其中:
- Subject(主体):与软件交互的一个特定的实体(用户、第三方服务等)。
- SecurityManager(安全管理器) :Shiro 的核心,用来协调管理组件工作。
- Authenticator(认证管理器):负责执行认证操作。
- Authorizer(授权管理器):负责授权检测。
- SessionManager(会话管理):负责创建并管理用户 Session 生命周期,提供一
个强有力的 Session 体验。 - SessionDAO:代表 SessionManager 执行 Session 持久(CRUD)动作,它允
许任何存储的数据挂接到 session 管理基础上。 - CacheManager(缓存管理器):提供创建缓存实例和管理缓存生命周期的功能。
- Cryptography(加密管理器):提供了加密方式的设计及管理。
- Realms(领域对象):是 shiro 和你的应用程序安全数据之间的桥梁。
2、Shiro 框架基础配置
Shiro 依赖 在项目中添加Shiro相关依赖(参考官网http://shiro.apache.org/spring-boot.html),假如项目中添加过shiro-spring依赖,将shiro-spring依赖替换掉即可。
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.7.0</version>
</dependency>
说明,添加完此依赖,直接启动项目会启动失败,还需要额外的配置。
Shiro 基本配置 第一步:创建一个Realm类型的实现类(基于此类通过DAO访问数据库),关键代码如下:
package com.cy.pj.sys.service.realm;
public class ShiroRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo( PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken authenticationToken) throws AuthenticationException {
return null;
}
}
第二步:在项目启动类中添加Realm对象配置,关键代码如下:
@Bean
public Realm realm(){
return new ShiroRealm();
}
第三步:在启动类中定义过滤规则(哪些访问路径要进行认证才可以访问),关键代码如下:
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
LinkedHashMap<String,String> map=new LinkedHashMap<>();
map.put("/bower_components/**","anon");
map.put("/build/**","anon");
map.put("/dist/**","anon");
map.put("/plugins/**","anon");
map.put("/**","authc");
chainDefinition.addPathDefinitions(map);
return chainDefinition;
}
第四步:配置认证页面(登录页面) 在spring的配置文件(application.yml)中,添加登录页面的配置,关键代码如下:
shiro:
loginUrl: /login.html
其中,login.html页面为项目中static目录定义好的一个页面。 第五步:启动服务进行访问测试 打开浏览器,输入http://localhost/doIndexUI检测是否会出现登录窗口,如图所示:
3、Shiro认证业务分析及实现
认证流程分析 当我们在登录页面,输入用户信息,提交到服务端进行认证,其中shiro框架的认证时序如图所示: 其中:
- token :封装用户提交的认证信息(例如用户名和密码)的一个对象。
- Subject: 负责将认证信息提交给SecurityManager对象的一个主体对象。
- SecurityManager是shiro框架的核心,负责完成其认证、授权等业务。
- Authenticator 认证管理器对象,SecurityManager继承了此接口。
- Realm 负责从数据库获取认证信息并交给认证管理器。
Shiro框架认证业务实现 第一步:在SysUserDao中定义基于用户名查询用户信息的方法,关键代码如下:
@Select("select * from sys_users where username=#{username}")
SysUser findUserByUsername(String username);
第二步:修改ShiroRealm中获取认证信息的方法,关键代码如下:
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken upToken=(UsernamePasswordToken) authenticationToken;
String username = upToken.getUsername();
SysUser sysUser = sysUserDao.findUserByUsername(username);
if (sysUser==null)
throw new UnknownAccountException();
if (sysUser.getValid()==0)throw new LockedAccountException();
ByteSource credentialsSalt=ByteSource.Util.bytes(sysUser.getSalt());
SimpleAuthenticationInfo info=
new SimpleAuthenticationInfo(
sysUser,
sysUser.getPassword(),
credentialsSalt,
getName());
return info;
}
第三步:在ShiroRealm中重谢获取凭证加密算法的方法,关键代码如下:
@Override
public CredentialsMatcher getCredentialsMatcher() {
HashedCredentialsMatcher matcher=new HashedCredentialsMatcher();
matcher.setHashAlgorithmName("MD5");
matcher.setHashIterations(1);
return matcher;
}
第四步:在SysUserController中添加处理登录请求的方法,关键代码如下:
@RequestMapping("doLogin")
public JsonResult doLogin(String username,String password){
UsernamePasswordToken token =
new UsernamePasswordToken(username, password);
Subject currentUser = SecurityUtils.getSubject();
currentUser.login(token);
return new JsonResult("login ok");
}
第五步:统一异常处理类中添加shiro异常处理代码,关键如下:
@ExceptionHandler(ShiroException.class)
public JsonResult doShiroException(ShiroException e){
JsonResult r=new JsonResult();
r.setState(0);
if (e instanceof UnknownAccountException){
r.setMessage("用户名不存在");
}else if (e instanceof IncorrectCredentialsException){
r.setMessage("密码不正确");
}else if (e instanceof LockedAccountException){
r.setMessage("账户被锁定");
}else if (e instanceof AuthorizationException){
r.setMessage("没有权限");
}else {
r.setMessage("认证或授权失败");
}
return r;
}
第五步:在过滤配置中允许登录时的url匿名访问,关键代码如下:
...
map.put("/user/doLogin","anon");
...
第六步:再过滤配置中配置登出url操作,关键代码如下:
..
map.put("/doLogout","logout");
...
第六步:启动服务器,进行登录访问测试 第七步:Shiro框架认证流程总结分析 Step01:登录客户端(login.html)中的用户输入的登录信息提交SysUserController对象 Step02:SysUserController对象基于doLogin方法处理登录请求. Step03:SysUserController中的doLogin方法将用户信息封装token中, 然后基于subject对象将 token提交给SecurityManager对象。 Step04:SecurityManager对象调用认证方法(authenticate)去完成认证,在此方法内部会调用 ShiroRealm中的doGetAuthenticationInfo获取数据库中的用户信息,然后再与客户端提交的 token中的信息进行比对,比对时会调用getCredentialsMatcher方法获取凭证加密对象,通过此对 象对用户提交的token中的密码进行加密。
4、Shiro授权业务分析及实现
授权流程分析 已认证用户,在进行系统资源的访问时,我们还要检查用户是否有这个资源的访问权限。并不是所有认证用户都可以访问系统内所有资源,也应该是受限访问的,具体授权流程如图所示: Shiro框架中的授权实现 第一步: 在SysMenuDao中定义查询用户权限标识的方法,关键代码分析:
Set<String> findUserPermissions(Integer userId);
第二步:在SysMenuMapper中添加查询用户权限标识的SQL映射,关键代码如下:
<select id="findUserPermissions" resultType="String">
select distinct permission
from sys_user_roles ur join sys_role_menus rm join sys_menus m
on ur.role_id=rm.role_id and rm.menu_id=m.id
where ur.user_id=#{userId} and trim(m.permission)!='' and m.permission is not null
</select>
第三步:修改ShiroRealm中获取权限并封装权限信息的方法,关键代码如下
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SysUser user =(SysUser) principalCollection.getPrimaryPrincipal();
Set<String> stringPermissions=sysMenuDao.findUserPermissions(user.getId());
SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
info.setStringPermissions(stringPermissions);
return info;
}
第四步:定义授权切入点方法,示例代码如下: 在shiro框架中,授权切入点方法需要通过**@RequiresPermissions**注解进行描述,例如:
@RequiresPermissions("sys:user:update")
public int validById(Integer id,Integer valid){
int rows=sysUserDao.validById(id,valid);
if(rows==0)throw new ServiceException("记录可能已经不存在");
return rows; }
其中, @RequiresPermissions注解中定义的内容为,访问此方法需要的权限. @RequiresPermissions 描述的方法为切入点方法,此方法在执行时需要在“通知方法”中判定用户是否由访问此方法的权限(检测用户权限中是否包含@RequiresPermissions注解内部包含的限),假如有权限,则授权访问.
第五步:启动服务进行访问测试和原理分析 在访问时首先要检测一下用户有什么权限,检测过程,先查询用户有什么角色,再查看角色有什么菜单的访问权限. 授权原理分析:(底层基于AOP实现) Step01 页面上用户通过菜单触发对服务端资源的访问. Step02 服务端Controller处理客户端的资源访问请求 Step03 假如客户端请求访问的资源业务放上有@RequiresPermissions注解描述则底层Controller 对象会调用Service的代理对象,代理对象会调用AOP中通知方法,在通知方法中获取@RequiresPermissions上的定义的权限标识. Step04 通过Subject 对象提交@RequiresPermissions注解中的授权标识给SecurityManager对象, 此对象会调用ShiroRealm中的获取用户权限的方法,最终会将从数据权限信息与 @RequiresPermissions中的定义的权限信息做一个比对.
5、Shiro认证授权业务进阶实现
呈现登录用户信息 Controller 方法定义及实现 修改PageController中的doIndex方法(主页调用的方法),关键代码如下:
@RequestMapping("doIndexUI")
public String doIndexUI(Model model) {
SysUser user=(SysUser)
SecurityUtils.getSubject().getPrincipal();
model.addAttribute("username", user.getUsername());
return "starter";
}
页面Thymeleaf 表达式应用 打开starter.html页面,找到用户名对应的位置,然后通过[[${}]]表达式获取服务端model中数据,呈现在页面上,关键代码如下:
<span class="hidden-xs" id="loginUserId">[[${username}]]</span>
用户菜单的动态化呈现 业务分析 我们希望不同登录用户,登录系统以后,看到的用户菜单是不一样的。登录用户只能看到自己可以访问的一些菜单选项
服务端设计及实现 第一步:定义pojo对象存储用户菜单信息,关键代码如下:
package com.cy.pj.sys.pojo;
@Data
public class SysUserMenu implements Serializable{
private static final long serialVersionUID = -410105494012229800L;
private Integer id;
private String name;
private String url;
private List<SysUserMenu> childMenus;
}
第二步:在SysMenuDao中定义查询用户一级和二级菜单信息的方法,关键代码如下:
List<SysUserMenu> findUserMenus(Integer userId);
第三步:在SysMenuMapper中定义查询用户一级和二级菜单信息时对应的sql映射,关键代码如下:
<resultMap id="sysUserMenu" type="com.cy.pj.sys.pojo.SysUserMenu">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="url" column="url"/>
<collection property="childMenus" ofType="com.cy.pj.sys.pojo.SysUserMenu">
<id property="id" column="cid"/>
<result property="name" column="cname"/>
<result property="url" column="curl"/>
</collection>
</resultMap>
<select id="findUserMenus" resultMap="sysUserMenu">
select p.id,p.name,p.url,c.id cid,c.name cname,c.url curl
from sys_menus p join sys_menus c
on p.id=c.parentId
where p.parentId is null and c.id in (
select rm.menu_id from sys_user_roles ur join sys_role_menus rm
on ur.role_id=rm.role_id
where ur.user_id=#{userId}
)
</select>
注:查询多级菜单可以根据递归查询,比如,先查询根据userid查询出全部菜单信息进行封装,然后根据一级菜单id查询出一级菜单进行封装,然后定义递归方法根据父菜单id进行查询子菜单信息封装到父级菜单,直到子菜单没有数据,结束。
第四步:在SysMenuService接口中添加查询用户菜单信息的方法,关键代码如下:
List<SysUserMenu> findUserMenus(Integer userId);
第五步:在SysMenuServiceImpl类中添加查询用户菜单信息的方法,关键代码如下:
public List<SysUserMenu> findUserMenus(Integer userId){
return sysMenuDao.findUserMenus(userId);
}
第六步:修改PageController中的doIndexUI方法,关键代码如下:
@Autowired private SysMenuService sysMenuService;
@RequestMapping("doIndexUI")
public String doIndexUI(Model model) {
SysUser user=(SysUser)
SecurityUtils.getSubject().getPrincipal();
model.addAttribute("username", user.getUsername());
List<SysUserMenu> userMenus = sysMenuService.findUserMenus(user.getId());
model.addAttribute("userMenus", userMenus);
return "starter";
}
客户端设计及实现 第一步:修改starter页面菜单呈现部分的内容,关键代码如下:
<li class="treeview" th:each="p:${userMenus}">
<a href="#"><i class="fa fa-link"></i>
<span>[[${p.name}]]</span>
<span class="pull-right-container">
<i class="fa fa-angle-left pull-right"></i>
</span>
</a>
<ul class="treeview-menu">
<li th:each="c:${p.childMenus}">
<a th:onclick="doLoadRS([[${c.url}]])">[[${c.name}]]</a></li>
</ul>
</li>
第二步:添加菜单事件处理函数,关键代码如下
function doLoadRS(url){ $("#mainContentId").load(url); }
第三步:启动服务进行访问测试
|