Shiro主要用来进行权限管理。简单的介绍如下:
一、概念
Shiro是一个安全框架,可以进行角色、权限管理。
Shiro主要功能如下:
Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
Web Support:Web支持,可以非常容易的集成到Web环境;
Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing:提供测试支持;
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
记住一点,Shiro不会去维护用户、维护权限;这些需要我们自己去设计/提供;然后通过相应的接口注入给Shiro即可。
二、主要的类
1.Subject:当前用户,Subject可以是一个人,也可以是第三方服务
2.SecurityManager:管理所有Subject,可以配合内部安全组件。
3.principals:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个principals,但只有一个Primary principals,一般是用户名/密码/手机号。
4.credentials:证明/凭证,即只有主体知道的安全值,如密码/数字证书等。
最常见的principals和credentials组合就是用户名/密码了。
5.Realms:用于进行权限信息的验证,需要自己实现。
6.Realm 本质上是一个特定的安全 DAO:它封装与数据源连接的细节,得到Shiro 所需的相关的数据。
在配置 Shiro 的时候,你必须指定至少一个Realm 来实现认证(authentication)和/或授权(authorization)。
我们需要实现Realms的Authentication 和 Authorization。其中 Authentication 是用来验证用户身份,Authorization 是授权访问控制,用于对用户进行的操作授权,证明该用户是否允许进行当前操作,如访问某个链接,某个资源文件等。
7.SimpleHash,可以通过特定算法(比如md5)配合盐值salt,对密码进行多次加密。
首先,我们从外部来看Shiro吧,即从应用程序角度的来观察如何使用Shiro完成工作。如下图:
可以看到:应用代码直接交互的对象是Subject,也就是说Shiro的对外API核心就是Subject;其每个API的含义:
Subject:主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者;
SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器;
Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。
接下来我们来从Shiro内部来看下Shiro的架构,如下图所示:
Subject:主体,可以看到主体可以是任何可以与应用交互的“用户”;
SecurityManager:相当于SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是Shiro的心脏;所有具体的交互都通过SecurityManager进行控制;它管理着所有Subject、且负责进行认证和授权、及会话、缓存的管理。
Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得Shiro默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
Authrizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
Realm:可以有1个或多个Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC实现,也可以是LDAP实现,或者内存实现等等;由用户提供;注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的Realm;
SessionManager:如果写过Servlet就应该知道Session的概念,Session呢需要有人去管理它的生命周期,这个组件就是SessionManager;而Shiro并不仅仅可以用在Web环境,也可以用在如普通的JavaSE环境、EJB等环境;所有呢,Shiro就抽象了一个自己的Session来管理主体与应用之间交互的数据;这样的话,比如我们在Web环境用,刚开始是一台Web服务器;接着又上了台EJB服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到Memcached服务器);
SessionDAO:DAO大家都用过,数据访问对象,用于会话的CRUD,比如我们想把Session保存到数据库,那么可以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把Session放到Memcached中,可以实现自己的Memcached SessionDAO;另外SessionDAO中可以使用Cache进行缓存,以提高性能;
CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能
Cryptography:密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密的。
三、Shiro配置
1.Spring集成Shiro一般通过xml配置,SpringBoot集成Shiro一般通过java代码配合@Configuration和@Bean配置。
2.Shiro的核心通过过滤器Filter实现。Shiro中的Filter是通过URL规则来进行过滤和权限校验,所以我们需要定义一系列关于URL的规则和访问权限。
3.SpringBoot集成Shiro,我们需要写的主要是两个类,ShiroConfiguration类,还有继承了AuthorizingRealm的Realm类
ShiroConfiguration类,用来配置Shiro,注入各种Bean。
包括过滤器(shiroFilter)、安全事务管理器(SecurityManager)、密码凭证(CredentialsMatcher)、aop注解支持(authorizationAttributeSourceAdvisor)等等
Realm类,包括登陆认证(doGetAuthenticationInfo)、授权认证(doGetAuthorizationInfo)
四、集成如下:
1.依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.3</version>
</dependency>
2.实体类
实体有三个,根据规则会自动生成两个中间表,数据库实际上会生成5张表。
分别是User,SysRole,SysPermission,中间表按照@JoinTable来生成。
@Entitypublicclass User {
@Id
@GenericGenerator(name="generator",strategy = "native")
@GeneratedValue(generator = "generator")
private Integer userId;
@Column(nullable =false, unique =true)
privateString userName;//登录用户名@Column(nullable =false)
privateString name;//名称(昵称或者真实姓名,根据实际情况定义)@Column(nullable =false)
private String password;
privateString salt;//加密密码的盐privatebytestate;//用户状态,0:创建未认证(比如没有激活,没有输入验证码等等)--等待验证的用户 , 1:正常 状态,2:用户被锁定.
@ManyToMany(fetch= FetchType.EAGER)//立即从数据库中进行加载数据;@JoinTable(name = "SysUserRole", joinColumns = {
@JoinColumn(name = "userId") }, inverseJoinColumns ={@JoinColumn(name = "roleId") })
privateList roleList;// 一个用户具有多个角色@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm")
privateLocalDateTime createTime;//创建时间@DateTimeFormat(pattern = "yyyy-MM-dd")
privateLocalDate expiredDate;//过期日期private String email;
public String getEmail() {
return email;
}
publicvoid setEmail(String email) {
this.email = email;
}
public LocalDateTime getCreateTime() {
return createTime;
}
publicvoid setCreateTime(LocalDateTime createTime) {
this.createTime = createTime;
}
public LocalDate getExpiredDate() {
return expiredDate;
}
publicvoid setExpiredDate(LocalDate expiredDate) {
this.expiredDate = expiredDate;
}
public Integer getUserId() {
return userId;
}
publicvoid setUserId(Integer userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
publicvoid setUserName(String userName) {
this.userName = userName;
}
public String getName() {
return name;
}
publicvoid setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
publicvoid setPassword(String password) {
this.password = password;
}
public String getSalt() {
return salt;
}
publicvoid setSalt(String salt) {
this.salt = salt;
}
publicbyte getState() {
return state;
}
publicvoidsetState(byte state) {
this.state = state;
}
publicList getRoleList() {
return roleList;
}
publicvoidsetRoleList(List roleList) {
this.roleList = roleList;
}
/*** 密码盐.重新对盐重新进行了定义,用户名+salt,这样就不容易被破解,可以采用多种方式定义加盐*@return*/public String getCredentialsSalt(){
returnthis.userName+this.salt;
}
}
@Entitypublicclass
SysRole {
@Id
@GenericGenerator(name="generator",strategy = "native")
@GeneratedValue(generator = "generator")
privateInteger roleId;// 编号@Column(nullable =false, unique =true)
privateString role;// 角色标识程序中判断使用,如"admin",这个是唯一的:privateString description;// 角色描述,UI界面显示使用privateBoolean available = Boolean.TRUE;// 是否可用,如果不可用将不会添加给用户
//角色 -- 权限关系:多对多关系;@ManyToMany(fetch= FetchType.EAGER)
@JoinTable(name="SysRolePermission",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="permissionId")})
privateList permissions;
// 用户 - 角色关系定义; @ManyToMany
@JoinTable(name="SysUserRole",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="userId")})
privateList users;// 一个角色对应多个用户public Integer getRoleId() {
return roleId;
}
publicvoid setRoleId(Integer roleId) {
this.roleId = roleId;
}
public String getRole() {
return role;
}
publicvoid setRole(String role) {
this.role = role;
}
public String getDescription() {
return description;
}
publicvoid setDescription(String description) {
this.description = description;
}
public Boolean getAvailable() {
return available;
}
publicvoid setAvailable(Boolean available) {
this.available = available;
}
publicList getPermissions() {
return permissions;
}
publicvoidsetPermissions(List permissions) {
this.permissions = permissions;
}
publicList getUsers() {
return users;
}
publicvoidsetUsers(List users) {
this.users = users;
}
}
@Entitypublicclass
SysPermission {
@Id
@GenericGenerator(name="generator",strategy = "native")
@GeneratedValue(generator = "generator")
privateInteger permissionId;//主键.@Column(nullable =false)
privateString permissionName;//名称.@Column(columnDefinition="enum('menu','button')")
privateString resourceType;//资源类型,[menu|button]privateString url;//资源路径.privateString permission;//权限字符串,menu例子:role:*,button例子:role:create,role:update,role:delete,role:viewprivateLong parentId;//父编号privateString parentIds;//父编号列表privateBoolean available = Boolean.TRUE;
//角色 -- 权限关系:多对多关系; @ManyToMany
@JoinTable(name="SysRolePermission",joinColumns={@JoinColumn(name="permissionId")},inverseJoinColumns={@JoinColumn(name="roleId")})
privateList roles;
public Integer getPermissionId() {
return permissionId;
}
publicvoid setPermissionId(Integer permissionId) {
this.permissionId = permissionId;
}
public String getPermissionName() {
return permissionName;
}
publicvoid setPermissionName(String permissionName) {
this.permissionName = permissionName;
}
public String getResourceType() {
return resourceType;
}
publicvoid setResourceType(String resourceType) {
this.resourceType = resourceType;
}
public String getUrl() {
return url;
}
publicvoid setUrl(String url) {
this.url = url;
}
public String getPermission() {
return permission;
}
publicvoid setPermission(String permission) {
this.permission = permission;
}
public Long getParentId() {
return parentId;
}
publicvoid setParentId(Long parentId) {
this.parentId = parentId;
}
public String getParentIds() {
return parentIds;
}
publicvoid setParentIds(String parentIds) {
this.parentIds = parentIds;
}
public Boolean getAvailable() {
return available;
}
publicvoid setAvailable(Boolean available) {
this.available = available;
}
publicList getRoles() {
return roles;
}
publicvoidsetRoles(List roles) {
this.roles = roles;
}
}
3.配置类
ShiroConfiguration.java如下
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.Cookie;import org.apache.shiro.web.servlet.SimpleCookie;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;
@Configurationpublicclass
public class ShiroConfiguration {
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();
//设置安全管理器 shiroFilterFactoryBean.setSecurityManager(securityManager);
//默认跳转到登陆页面shiroFilterFactoryBean.setLoginUrl("/login");
//登陆成功后的页面shiroFilterFactoryBean.setSuccessUrl("/index");
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
//自定义过滤器Map filterMap=newLinkedHashMap<>();
shiroFilterFactoryBean.setFilters(filterMap);
//权限控制mapMap filterChainDefinitionMap=newLinkedHashMap<>();
// 配置不会被拦截的链接 顺序判断filterChainDefinitionMap.put("/static/**", "anon");
//配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
filterChainDefinitionMap.put("/logout", "logout");
//<!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;////<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
// filterChainDefinitionMap.put("/**", "anon");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/*** 核心的安全事务管理器
* @return*/
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager ();
//设置realm securityManager.setRealm( myShiroRealm( ) );
securityManager.setRememberMeManager(rememberMeManager());
return securityManager;
}
/** * 身份认证Realm,此处的注入不可以缺少。否则会在UserRealm中注入对象会报空指针.
* @return*/
@Bean
public UserRealm myShiroRealm( ){
UserRealm myShiroRealm =new UserRealm();
myShiroRealm.setCredentialsMatcher( hashedCredentialsMatcher() );
return myShiroRealm;
}
/** * 哈希密码比较器。在myShiroRealm中作用参数使用
* 登陆时会比较用户输入的密码,跟数据库密码配合盐值salt解密后是否一致。
* @return*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher =new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用md5算法;
hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5( md5(""));
return hashedCredentialsMatcher;
}
////注入缓存
// @Bean
// public EhCacheManager ehCacheManager(){
// System.out.println("ShiroConfiguration.getEhCacheManager()执行");
// EhCacheManager cacheManager=new EhCacheManager();
// cacheManager.setCacheManagerConfigFile("classpath:config/ehcache-shiro.xml");
// return cacheManager;
// }
/** * 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;否则@RequiresRoles等注解无法生效
* @param securityManager
* @return*/ @Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor =new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/** * Shiro生命周期处理器
* @return*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
returnnew LifecycleBeanPostProcessor();
}
/** * 自动创建代理
* @return*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator =new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
}
Realm类如下:
package com.example.demo.config;
import com.example.demo.pojo.SysPermission;
import com.example.demo.pojo.SysUserRole;
import com.example.demo.pojo.User;
import com.example.demo.service.UserSerevice;
import com.example.demo.utils.State;
import org.apache.log4j.Logger;importorg.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource
;import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;import java.util.Set;
import static org.apache.coyote.http11.Constants.a;
publicclassUserRealmextends AuthorizingRealm {
@Resource(name = "userServiceImp")
private UserSerevice userService;
privateLogger logger=Logger.getLogger(UserRealm.class);
/*** 提供用户信息,返回权限信息
* @param principals
* @return*/ @Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
logger.info("---------------------------->授权认证:");
SimpleAuthorizationInfo authorizationInfo=new SimpleAuthorizationInfo();
String userName=(String) principals.getPrimaryPrincipal();
String userId=userService.findUserIdByName(userName);
Set roleIdSet=userService.findRoleIdByUid( Integer.parseInt(userId) );
Set roleSet=newHashSet<>();
Set pemissionIdSet=newHashSet<>();
Set pemissionSet=newHashSet<>();
for(SysUserRole roleInfo : roleIdSet) {
introleId=roleInfo.getRoleId();
roleSet.add( userService.findRoleByRoleId( roleId ) );
//将拥有角色的所有权限放进Set里面,也就是求Set集合的并集
//由于我这边的数据表设计得不太好,所以提取set集合比较麻烦
pemissionIdSet.addAll( userService.findPermissionIdByRoleId( roleId ))}
for(int permissionId : pemissionIdSet) {
String permission= userService.findPermissionById( permissionId ).getPermission() ;
pemissionSet.add( permission );
}
// 将角色名称组成的Set提供给授权info
authorizationInfo.setRoles( roleSet );
// 将权限名称组成的Set提供给info
authorizationInfo.setStringPermissions(pemissionSet);
return authorizationInfo;
}
/*** 提供帐户信息,返回认证信息
* @param authenticationToken
* @return*@throws AuthenticationException
*/ @Override
protectedAuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)throws AuthenticationException {
logger.info("---------------------------->登陆验证:");
String userName=(String)authenticationToken.getPrincipal();
User user=userService.findUserByName(userName);
if(user==null) {
//用户不存在就抛出异常
thrownew UnknownAccountException();
}
if( State.LOCKED.equals( user.getState() ) ) {
//用户被锁定就抛异常
thrownew LockedAccountException();
}
//密码可以通过SimpleHash加密,然后保存进数据库。
//此处是获取数据库内的账号、密码、盐值,保存到登陆信息info中
SimpleAuthenticationInfo authenticationInfo=new SimpleAuthenticationInfo(user.getUsername(),
user.getPassword(),
ByteSource.Util.bytes(user.getSalt()) ,
getName());
//realm namereturn authenticationInfo;
}
}