这篇文章咱们来学习如何使用Spring Boot集成Apache Shiro。安全应该是互联网公司的一道生命线,几乎任何的公司都会涉及到这方面的需求。在Java领域通常有Spring Security、Apache Shiro等安全框架,可是因为Spring Security过于庞大和复杂,大多数公司会选择Apache Shiro来使用,这篇文章会先介绍一下Apache Shiro,在结合Spring Boot给出使用案例。html
Apache Shiro是一个功能强大、灵活的,开源的安全框架。它能够干净利落地处理身份验证、受权、企业会话管理和加密。java
Apache Shiro的首要目标是易于使用和理解。安全一般很复杂,甚至让人感到很痛苦,可是Shiro却不是这样子的。一个好的安全框架应该屏蔽复杂性,向外暴露简单、直观的API,来简化开发人员实现应用程序安全所花费的时间和精力。mysql
Shiro能作什么呢?git
验证用户身份github
用户访问权限控制,好比:一、判断用户是否分配了必定的安全角色。二、判断用户是否被授予完成某个操做的权限web
在非 web 或 EJB 容器的环境下能够任意使用Session API算法
能够响应认证、访问控制,或者 Session 生命周期中发生的事件spring
可将一个或以上用户安全数据源数据组合成一个复合的用户 "view"(视图)sql
支持单点登陆(SSO)功能数据库
支持提供“Remember Me”服务,获取用户关联信息而无需登陆
…
等等——都集成到一个有凝聚力的易于使用的API。
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 是受权访问控制,用于对用户进行的操做受权,证实该用户是否容许进行当前操做,如访问某个连接,某个资源文件等。
pom包依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>net.sourceforge.nekohtml</groupId>
<artifactId>nekohtml</artifactId>
<version>1.9.22</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
重点是 shiro-spring包
配置文件
spring:
datasource:
url: jdbc:mysql://localhost:3306/test
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
jpa:
database: mysql
show-sql: true
hibernate:
ddl-auto: update
naming:
strategy: org.hibernate.cfg.DefaultComponentSafeNamingStrategy
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL5Dialect
thymeleaf:
cache: false
mode: LEGACYHTML5
thymeleaf的配置是为了去掉html的校验
页面
咱们新建了六个页面用来测试:
index.html :首页
login.html :登陆页
userInfo.html : 用户信息页面
userInfoAdd.html :添加用户页面
userInfoDel.html :删除用户页面
403.html : 没有权限的页面
除过登陆页面其它都很简单,大概以下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>index</h1>
</body>
</html>
RBAC 是基于角色的访问控制(Role-Based Access Control )在 RBAC 中,权限与角色相关联,用户经过成为适当角色的成员而获得这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便。
采用jpa技术来自动生成基础表格,对应的entity以下:
用户信息
@Entity
public class UserInfo implements Serializable {
@Id
@GeneratedValue
private Integer uid;
@Column(unique =true)
private String username;//账号
private String name;//名称(昵称或者真实姓名,不一样系统不一样定义)
private String password; //密码;
private String salt;//加密密码的盐
private byte state;//用户状态,0:建立未认证(好比没有激活,没有输入验证码等等)--等待验证的用户 , 1:正常状态,2:用户被锁定.
@ManyToMany(fetch= FetchType.EAGER)//当即从数据库中进行加载数据;
@JoinTable(name = "SysUserRole", joinColumns = { @JoinColumn(name = "uid") }, inverseJoinColumns ={@JoinColumn(name = "roleId") })
private List<SysRole> roleList;// 一个用户具备多个角色
// 省略 get set 方法
}
角色信息
@Entity
public class SysRole {
@Id@GeneratedValue
private Integer id; // 编号
private String role; // 角色标识程序中判断使用,如"admin",这个是惟一的:
private String description; // 角色描述,UI界面显示使用
private Boolean available = Boolean.FALSE; // 是否可用,若是不可用将不会添加给用户
//角色 -- 权限关系:多对多关系;
@ManyToMany(fetch= FetchType.EAGER)
@JoinTable(name="SysRolePermission",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="permissionId")})
private List<SysPermission> permissions;
// 用户 - 角色关系定义;
@ManyToMany
@JoinTable(name="SysUserRole",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="uid")})
private List<UserInfo> userInfos;// 一个角色对应多个用户
// 省略 get set 方法
}
权限信息
@Entity
public class SysPermission implements Serializable {
@Id@GeneratedValue
private Integer id;//主键.
private String name;//名称.
@Column(columnDefinition="enum('menu','button')")
private String resourceType;//资源类型,[menu|button]
private String url;//资源路径.
private String permission; //权限字符串,menu例子:role:*,button例子:role:create,role:update,role:delete,role:view
private Long parentId; //父编号
private String parentIds; //父编号列表
private Boolean available = Boolean.FALSE;
@ManyToMany
@JoinTable(name="SysRolePermission",joinColumns={@JoinColumn(name="permissionId")},inverseJoinColumns={@JoinColumn(name="roleId")})
private List<SysRole> roles;
// 省略 get set 方法
}
根据以上的代码会自动生成userinfo(用户信息表)、sysrole(角色表)、syspermission(权限表)、sysuserrole(用户角色表)、sysrole_permission(角色权限表)这五张表,为了方便测试咱们给这五张表插入一些初始化数据:
INSERT INTO `sys_permission` (`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`) VALUES (1,0,'用户管理',0,'0/','userInfo:view','menu','userInfo/userList');
INSERT INTO `sys_permission` (`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`) VALUES (2,0,'用户添加',1,'0/1','userInfo:add','button','userInfo/userAdd');
INSERT INTO `sys_permission` (`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`) VALUES (3,0,'用户删除',1,'0/1','userInfo:del','button','userInfo/userDel');
INSERT INTO `sys_role` (`id`,`available`,`description`,`role`) VALUES (1,'0','管理员','admin');
INSERT INTO `sys_role` (`id`,`available`,`description`,`role`) VALUES (2,'0','VIP会员','vip');INSERT INTO `sys_role_permission` VALUES ('1', '1');
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (1,1);
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (1,2);
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (1,3);
INSERT INTO `sys_user_role` (`role_id`,`uid`) VALUES (1,1);
INSERT INTO `user_info` (`uid`,`username`,`name`,`password`,`salt`,`state`) VALUES ('1', 'admin', '管理员', 'd3c59d25033dbf980d29554025c23a75', '8d78869f470951332959580424d4bf4f', 0);
首先要配置的是ShiroConfig类,Apache Shiro 核心经过 Filter 来实现,就好像SpringMvc 经过DispachServlet 来主控制同样。 既然是使用 Filter 通常也就能猜到,是经过URL规则来进行过滤和权限校验,因此咱们须要定义一系列关于URL的规则和访问权限。
ShiroConfig
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
System.out.println("ShiroConfiguration.shirFilter()");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//拦截器.
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();
// 配置不会被拦截的连接 顺序判断
filterChainDefinitionMap.put("/static/**", "anon");
//配置退出 过滤器,其中的具体的退出代码Shiro已经替咱们实现了
filterChainDefinitionMap.put("/logout", "logout");
//<!-- 过滤链定义,从上向下顺序执行,通常将/**放在最为下边 -->:这是一个坑呢,一不当心代码就很差使了;
//<!-- authc:全部url都必须认证经过才能够访问; anon:全部url都均可以匿名访问-->
filterChainDefinitionMap.put("/**", "authc");
// 若是不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/login");
// 登陆成功后要跳转的连接
shiroFilterFactoryBean.setSuccessUrl("/index");
//未受权界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public MyShiroRealm myShiroRealm(){
MyShiroRealm myShiroRealm = new MyShiroRealm();
return myShiroRealm;
}
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
return securityManager;
}
}
Filter Chain定义说明:
一、一个URL能够配置多个Filter,使用逗号分隔
二、当设置多个过滤器时,所有验证经过,才视为经过
三、部分过滤器可指定参数,如perms,roles
Shiro内置的FilterChain
Filter Name | Class |
---|---|
anon | org.apache.shiro.web.filter.authc.AnonymousFilter |
authc | org.apache.shiro.web.filter.authc.FormAuthenticationFilter |
authcBasic | org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter |
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 |
anon:全部url都均可以匿名访问
authc: 须要认证才能进行访问
user:配置记住我或认证经过能够访问
登陆认证明现
在认证、受权内部实现机制中都有提到,最终处理都将交给Real进行处理。由于在Shiro中,最终是经过Realm来获取应用程序中的用户、角色及权限信息的。一般状况下,在Realm中会直接从咱们的数据源中获取Shiro须要的验证信息。能够说,Realm是专用于安全框架的DAO. Shiro的认证过程最终会交由Realm执行,这时会调用Realm的 getAuthenticationInfo(token)方法。
该方法主要执行如下操做:
一、检查提交的进行认证的令牌信息
二、根据令牌信息从数据源(一般为数据库)中获取用户信息
三、对用户信息进行匹配验证。
四、验证经过将返回一个封装了用户信息的 AuthenticationInfo实例。
五、验证失败则抛出 AuthenticationException异常信息。
而在咱们的应用程序中要作的就是自定义一个Realm类,继承AuthorizingRealm抽象类,重载doGetAuthenticationInfo(),重写获取用户信息的方法。
doGetAuthenticationInfo的重写
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
System.out.println("MyShiroRealm.doGetAuthenticationInfo()");
//获取用户的输入的帐号.
String username = (String)token.getPrincipal();
System.out.println(token.getCredentials());
//经过username从数据库中查找 User对象,若是找到,没找到.
//实际项目中,这里能够根据实际状况作缓存,若是不作,Shiro本身也是有时间间隔机制,2分钟内不会重复执行该方法
UserInfo userInfo = userInfoService.findByUsername(username);
System.out.println("----->>userInfo="+userInfo);
if(userInfo == null){
return null;
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
userInfo, //用户名
userInfo.getPassword(), //密码
ByteSource.Util.bytes(userInfo.getCredentialsSalt()),//salt=username+salt
getName() //realm name
);
return authenticationInfo;
}
连接权限的实现
shiro的权限受权是经过继承 AuthorizingRealm抽象类,重载 doGetAuthorizationInfo();当访问到页面的时候,连接配置了相应的权限或者shiro标签才会执行此方法不然不会执行,因此若是只是简单的身份认证没有权限的控制的话,那么这个方法能够不进行实现,直接返回null便可。在这个方法中主要是使用类: SimpleAuthorizationInfo进行角色的添加和权限的添加。
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("权限配置-->MyShiroRealm.doGetAuthorizationInfo()");
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
UserInfo userInfo = (UserInfo)principals.getPrimaryPrincipal();
for(SysRole role:userInfo.getRoleList()){
authorizationInfo.addRole(role.getRole());
for(SysPermission p:role.getPermissions()){
authorizationInfo.addStringPermission(p.getPermission());
}
}
return authorizationInfo;
}
固然也能够添加set集合:roles是从数据库查询的当前用户的角色,stringPermissions是从数据库查询的当前用户对应的权限
authorizationInfo.setRoles(roles);
authorizationInfo.setStringPermissions(stringPermissions);
就是说若是在shiro配置文件中添加了 filterChainDefinitionMap.put(“/add”,“perms[权限添加]”);就说明访问/add这个连接必需要有“权限添加”这个权限才能够访问,若是在shiro配置文件中添加了 filterChainDefinitionMap.put(“/add”,“roles[100002],perms[权限添加]”);就说明访问 /add这个连接必需要有“权限添加”这个权限和具备“100002”这个角色才能够访问。
登陆实现
登陆过程其实只是处理异常的相关信息,具体的登陆验证交给shiro来处理
@RequestMapping("/login")
public String login(HttpServletRequest request, Map<String, Object> map) throws Exception{
System.out.println("HomeController.login()");
// 登陆失败从request中获取shiro处理的异常信息。
// shiroLoginFailure:就是shiro异常类的全类名.
String exception = (String) request.getAttribute("shiroLoginFailure");
System.out.println("exception=" + exception);
String msg = "";
if (exception != null) {
if (UnknownAccountException.class.getName().equals(exception)) {
System.out.println("UnknownAccountException -- > 帐号不存在:");
msg = "UnknownAccountException -- > 帐号不存在:";
} else if (IncorrectCredentialsException.class.getName().equals(exception)) {
System.out.println("IncorrectCredentialsException -- > 密码不正确:");
msg = "IncorrectCredentialsException -- > 密码不正确:";
} else if ("kaptchaValidateFailed".equals(exception)) {
System.out.println("kaptchaValidateFailed -- > 验证码错误");
msg = "kaptchaValidateFailed -- > 验证码错误";
} else {
msg = "else >> "+exception;
System.out.println("else -- >" + exception);
}
}
map.put("msg", msg);
// 此方法不处理登陆成功,由shiro进行处理
return "/login";
}
其它dao层和service的代码就不贴出来了你们直接看代码。
一、编写好后就能够启动程序,访问index页面,因为没有登陆就会跳转到login页面。登陆以后就会跳转到index页面,登陆后,有直接在浏览器中输入index页面访问,又会跳转到login页面。上面这些操做时候触发 MyShiroRealm.doGetAuthenticationInfo()这个方法,也就是登陆认证的方法。
二、登陆admin帐户,访问: http://127.0.0.1:8080/userInfo/userAdd显示 用户添加界面,访问 http://127.0.0.1:8080/userInfo/userDel显示 403没有权限。上面这些操做时候触发 MyShiroRealm.doGetAuthorizationInfo()这个方面,也就是权限校验的方法。
三、修改admin不一样的权限进行测试
shiro很强大,这仅仅是完成了登陆认证和权限管理这两个功能,更多内容之后有时间再作探讨。
示例代码:https://github.com/ityouknow/spring-boot-examples