一 Shiro介绍
Apache Shiro是一个功能强大、灵活的,开源的安全框架。它可以干净利落地处理身份验证、授权、企业会话管理和加密。
Apache Shiro的首要目标是易于使用和理解。安全通常很复杂,甚至让人感到很痛苦,但是Shiro却不是这样子的。一个好的安全框架应该屏蔽复杂性,
向外暴露简单、直观的API,来简化开发人员实现应用程序安全所花费的时间和精力。
Shiro能做什么呢:
1. 验证用户身份
2. 用户访问权限控制,比如:1、判断用户是否分配了一定的安全角色。2、判断用户是否被授予完成某个操作的权限
3. 在非 web 或 EJB 容器的环境下可以任意使用Session API
4. 可以响应认证、访问控制,或者 Session 生命周期中发生的事件
5. 可将一个或以上用户安全数据源数据组合成一个复合的用户 “view”(视图)
6. 支持单点登录(SSO)功能
7. 支持提供“Remember Me”服务,获取用户关联信息而无需登录
二 Apache Shiro Features 特性
Apache Shiro是一个全面的、蕴含丰富功能的安全框架。上图为描述Shiro功能的框架图:
Authentication(认证), Authorization(授权), Session Management(会话管理), Cryptography(加密)被 Shiro 框架的开发团队称之为
应用安全的四大基石:
1. Authentication(认证):用户身份识别,通常被称为用户“登录”
2. Authorization(授权):访问控制。比如某个用户是否具有某个操作的使用权限。
3. Session Management(会话管理):特定于用户的会话管理,甚至在非web 或 EJB 应用程序。
4. Cryptography(加密):在对数据源使用加密算法加密的同时,保证易于使用。
还有其他的功能来支持和加强这些不同应用环境下安全领域的关注点。特别是对以下的功能支持:
1. Web支持:Shiro 提供的 web 支持 api ,可以很轻松的保护 web 应用程序的安全。
2. 缓存:缓存是 Apache Shiro 保证安全操作快速、高效的重要手段。
3. 并发:Apache Shiro 支持多线程应用程序的并发特性。
4. 测试:支持单元测试和集成测试,确保代码和预想的一样安全。
5. “Run As”:这个功能允许用户假设另一个用户的身份(在许可的前提下)。
6. “Remember Me”:跨 session 记录用户的身份,只有在强制需要时才需要登录。
注意: Shiro不会去维护用户、维护权限,这些需要我们自己去设计/提供,然后通过相应的接口注入给Shiro。
三 High-Level Overview 高级概述
在概念层,Shiro 架构包含三个主要的理念:Subject,SecurityManager和Realm。上面的图展示了这些组件如何相互作用,我们将在下面依次对其进行描述。
1. Subject:当前用户,Subject可以是一个人,但也可以是第三方服务、守护进程帐户、时钟守护任务或者其它–当前和软件交互的任何事件。
2. SecurityManager:管理所有Subject,SecurityManager是Shiro架构的核心,配合内部安全组件共同组成安全伞。
3. Realms:用于进行权限信息的验证,我们自己实现。Realm本质上是一个特定的安全DAO:它封装与数据源连接的细节,得到Shiro所需的相关的数据。
在配置Shiro的时候,你必须指定至少一个Realm来实现认证(authentication)和/或授权(authorization)。
我们需要实现Realms的Authentication和Authorization。其中Authentication是用来验证用户身份,
Authorization 是授权访问控制,用于对用户进行的操作授权,证明该用户是否允许进行当前操作,如访问某个链接,某个资源文件等。
四 依赖导入
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
五 创建实体类(映射到数据库)
(一)UserInfo类
package com.study.springboot.SpringBoot_A.bean;
import javax.persistence.*;
import java.io.Serializable;
import java.util.List;
/**
* @author 赵超
* @Description
* @create 2018-03-31 14:26
**/
@Entity
public class UserInfo implements Serializable {
private static final long serialVersionUID = 187634918317521L;
@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;//一个用户具有多个角色
public Integer getUid() { return uid;
}
public void setUid(Integer uid) { this.uid = uid;
}
public String getUsername() { return username;
}
public void setUsername(String username) { this.username = username;
}
public String getName() { return name;
}
public void setName(String name) { this.name = name;
}
public String getPassword() { return password;
}
public void setPassword(String password) { this.password = password;
}
public String getSalt() { return salt;
}
public void setSalt(String salt) { this.salt = salt;
}
public byte getState() { return state;
}
public void setState(byte state) { this.state = state;
}
public List<SysRole> getRoleList() { return roleList;
}
public void setRoleList(List<SysRole> roleList) { this.roleList = roleList;
}
@Override
public String toString() { return "UserInfo{" + "uid=" + uid +
", username='" + username + '\'' + ", name='" + name + '\'' +
", password='" + password + '\'' + ", salt='" + salt + '\'' +
", state=" + state + ", roleList=" + roleList + '}';
}
public byte[] getCredentialsSalt() { return (this.salt + this.username).getBytes();
}
}
(二)用户角色类
package com.study.springboot.SpringBoot_A.bean;
import javax.persistence.*;
import java.io.Serializable;
import java.util.List;
/**
* @author 赵超
* @Description
* @create 2018-03-31 14:37
**/
@Entity
public class SysRole implements Serializable {
private static final long serialVersionUID = 194534918317521L;
@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;
public Integer getId() { return id;
}
public void setId(Integer id) { this.id = id;
}
public String getRole() { return role;
}
public void setRole(String role) { this.role = role;
}
public String getDescription() { return description;
}
public void setDescription(String description) { this.description = description;
}
public Boolean getAvailable() { return available;
}
public void setAvailable(Boolean available) { this.available = available;
}
public List<SysPermission> getPermissions() { return permissions;
}
public void setPermissions(List<SysPermission> permissions) { this.permissions = permissions;
}
public List<UserInfo> getUserInfos() { return userInfos;
}
public void setUserInfos(List<UserInfo> userInfos) { this.userInfos = userInfos;
}
}
(三)权限类
package com.study.springboot.SpringBoot_A.bean;
import javax.persistence.*;
import java.io.Serializable;
import java.util.List;
/**
* @author 赵超
* @Description
* @create 2018-03-31 14:50
**/
@Entity
public class SysPermission implements Serializable {
private static final long serialVersionUID = 187634922317521L;
@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;
public Integer getId() { return id;
}
public void setId(Integer id) { this.id = id;
}
public String getName() { return name;
}
public void setName(String name) { this.name = name;
}
public String getResourceType() { return resourceType;
}
public void setResourceType(String resourceType) { this.resourceType = resourceType;
}
public String getUrl() { return url;
}
public void setUrl(String url) { this.url = url;
}
public String getPermission() { return permission;
}
public void setPermission(String permission) { this.permission = permission;
}
public Long getParentId() { return parentId;
}
public void setParentId(Long parentId) { this.parentId = parentId;
}
public String getParentIds() { return parentIds;
}
public void setParentIds(String parentIds) { this.parentIds = parentIds;
}
public Boolean getAvailable() { return available;
}
public void setAvailable(Boolean available) { this.available = available;
}
public List<SysRole> getRoles() { return roles;
}
public void setRoles(List<SysRole> roles) { this.roles = roles;
}
}
稍后运行之后自动生成user_info(用户信息表)、sys_role(角色表)、sys_permission(权限表)、
sys_user_role(用户角色表)、sys_role_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', '管理员', '111111', '111111', 0);
六 Shiro配置类
说明:Shiro 核心通过 Filter 来实现,就好像SpringMvc 通过DispachServlet 来主控制一样。
package com.study.springboot.SpringBoot_A.shiro;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.mgt.SecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author 赵超
* @Description shiro配置类
* @create 2018-03-31 15:03
**/
@Configuration
public class ShiroConfig {
/**
* 注意这里的SecurityManager是org.apache.shiro.mgt.SecurityManager,而不是java.lang.SecurityManager
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
System.out.println("shiroFilterFactoryBean");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager); //拦截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); //配置不会被拦截的链接,顺序判断
//authc:所有url都必须认证通过才可以访问;anno:所有url都可以匿名访问;user:配置记住我或认证通过可以访问
filterChainDefinitionMap.put("/static/**", "anon"); //配置退出过滤器,具体的退出代码shiro已经帮我们实现了
filterChainDefinitionMap.put("/logout", "logout"); //过滤链定义,从上向下顺序执行,一般将‘/**’放在最下面(注意,否则可能会出现异常)
filterChainDefinitionMap.put("/**", "authc"); //如果不设置会默认查找项目根目录下的/login.jsp页面
shiroFilterFactoryBean.setLoginUrl("/login"); //登陆成功之后需要跳转的连接诶
shiroFilterFactoryBean.setSuccessUrl("/index"); //未授权页面
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 凭证匹配器
* (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
* 所以我们需要修改下doGetAuthenticationInfo中的代码;(如果直接使用明文密码请忽略)
* )
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5(""));
return hashedCredentialsMatcher;
}
@Bean
public MyShiroRealm myShiroRealm() {
System.out.println("realm");
MyShiroRealm myShiroRealm = new MyShiroRealm();
// myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());//注释掉 测试方便使用明文密码
return myShiroRealm;
}
@Bean
public SecurityManager securityManager() {
System.out.println("securityManager");
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(myShiroRealm()); return defaultWebSecurityManager;
}
/**
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
Filter Chain定义说明:
1. 一个URL可以配置多个Filter,使用逗号分隔
2. 当设置多个过滤器时,全部验证通过,才视为通过
3. 部分过滤器可指定参数,如perms,roles
Shiro内置的FilterChain:
1. anon:所有url都都可以匿名访问
2. authc: 需要认证才能进行访问
3. user:配置记住我或认证通过可以访问
七 自定义认证类
链接权限的实现
shiro的权限授权是通过继承AuthorizingRealm抽象类,重载doGetAuthorizationInfo();当访问到页面的时候,
链接配置了相应的权限或者shiro标签才会执行此方法否则不会执行,所以如果只是简单的身份认证没有权限的控制的话,
那么这个方法可以不进行实现,直接返回null即可。在这个方法中主要是使用类:SimpleAuthorizationInfo进行角色的添加和权限的添加。
登录认证实现
在认证、授权内部实现机制中都有提到,最终处理都将交给Real进行处理。因为在Shiro中,最终是通过Realm来获取应用程序中的用户、角色及权限信息的。
通常情况下,在Realm中会直接从我们的数据源中获取Shiro需要的验证信息。
可以说,Realm是专用于安全框架的DAO. Shiro的认证过程最终会交由Realm执行,这时会调用Realm的getAuthenticationInfo(token)方法。
该方法主要执行以下操作:
1. 检查提交的进行认证的令牌信息
2. 根据令牌信息从数据源(通常为数据库)中获取用户信息
3. 对用户信息进行匹配验证。
4. 验证通过将返回一个封装了用户信息的AuthenticationInfo实例。
5. 验证失败则抛出AuthenticationException异常信息。
而在我们的应用程序中要做的就是自定义一个Realm类,继承AuthorizingRealm抽象类,重载doGetAuthenticationInfo(),重写获取用户信息的方法。
MyShiroRealm类:
package com.study.springboot.SpringBoot_A.shiro;
import com.study.springboot.SpringBoot_A.bean.SysPermission;
import com.study.springboot.SpringBoot_A.bean.SysRole;
import com.study.springboot.SpringBoot_A.bean.UserInfo;
import com.study.springboot.SpringBoot_A.service.UserInfoService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
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 org.springframework.beans.factory.annotation.Autowired;
/**
* @author 赵超
* @Description
* @create 2018-03-31 15:19
**/
public class MyShiroRealm extends AuthorizingRealm {
@Autowired
private UserInfoService userInfoService;
/**
* 链接权限的实现shiro的权限授权是通过继承AuthorizingRealm抽象类,重载doGetAuthorizationInfo();
* 当访问到页面的时候,链接配置了相应的权限或者shiro标签才会执行此方法否则不会执行,
* 所以如果只是简单的身份认证没有权限的控制的话,那么这个方法可以不进行实现,直接返回null即可。
* 在这个方法中主要是使用类:SimpleAuthorizationInfo进行角色的添加和权限的添加。
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("权限配置-->MyShiroRealm.doGetAuthorizationInfo()");
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
UserInfo userInfo = (UserInfo) principalCollection.getPrimaryPrincipal();
for (SysRole role : userInfo.getRoleList()) {
authorizationInfo.addRole(role.getRole());
for (SysPermission p : role.getPermissions()) {
authorizationInfo.addStringPermission(p.getPermission()); /*
当然也可以添加set集合:
roles是从数据库查询的当前用户的角色,
stringPermissions是从数据库查询的当前用户对应的权限
authorizationInfo.setRoles(roles);
authorizationInfo.setStringPermissions(stringPermissions);
*/
}
}
return authorizationInfo;
}
/**
* 重写获取用户信息的方法
*
* @param authenticationToken
* @return
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("MyShiroRealm.doGetAuthenticationInfo"); //获取用户输入的账号
String username = (String) authenticationToken.getPrincipal();
System.out.println(authenticationToken.getCredentials()); //通过username在数据库中查找UserInfo对象
//这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
UserInfo userInfo = userInfoService.findByUsername(username);
System.out.println("userInfo: " + userInfo);
if (null == userInfo) {
return null;
}
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
userInfo,//用户名
userInfo.getPassword(),//密码
// ByteSource.Util.bytes(userInfo.getCredentialsSalt()),//salt=username+salt
ByteSource.Util.bytes(userInfo.getSalt()),//salt=username+salt
getName());
SecurityUtils.getSubject().getSession().setAttribute("login", userInfo);
return simpleAuthenticationInfo;
}
}
八 前台页面编写
编写四个简单页面 userInfoAdd.html、userList.html、userInfoDel.html、userInfo.html;
编写一个登录页login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>denglu</title>
</head>
<body>
登录页面<br/>
<form action="/login" method="post" >
<input type="text" name="username">请输入登录名</input>
<input type="password" name="password">请输入密码</input>
<input type="submit" value="登录"/>
<div th:text="${msg}"></div>
</form>
</body>
</html>
九 登录controller
@RequestMapping(value = "/login", method = RequestMethod.POST)//shiro这里只能处理post请求,添加限制有助于理解
public String login(HttpServletRequest request, Map<String, Object> map) throws Exception{
System.out.println("ThymeleafController.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";
}
十 测试操作controller
前缀 /userInfo
/**
* 用户查询.
* @return
*/
@RequestMapping("/userList")
public String userInfo(){
return "userInfo";
}
/**
* 用户添加;
* @return
*/
@RequestMapping("/userAdd")
@RequiresPermissions("userInfo:add")//权限管理;
public String userInfoAdd(){
return "userInfoAdd";
}
/**
* 用户删除;
* @return
*/
@RequestMapping("/userDel")
@RequiresPermissions("userInfo:del")//权限管理;
public String userDel(){
return "userInfoDel";
}