今天研究一下 常用的安全框架shiro
先好好研究一下shiro 的首先原理,方便后面的学习
shiro (java安全框架)
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以
快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
软件名称 Apache Shiro 开发商 Apache 性质 Java安全框架
复制代码
主要功能 三个核心组件:Subject, SecurityManager 和 Realms.
Subject:即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户
(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。但考虑到大多数目的和用途,你可以把它认为
是Shiro的“用户”概念。Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。
SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来
提供安全管理的各种服务。
Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控
制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。从这个意义上讲,Realm实质上是一个安全相关的DAO:它
封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(
或)授权。配置多个Realm是可以的,但是至少需要一个。 Shiro内置了可以连接大量安全数据源(又名目录)的Realm,
如LDAP、关系数据库(JDBC)、类似INI的文本配置资源以及属性文件等。如果缺省的Realm不能满足需求,你还可以插入代表
自定义数据源的自己的Realm实现。
复制代码
shiro 原理剖析: shiro的核心是java servlet规范中的filter,通过配置拦截器,使用拦截器链来拦截请求,如果允许访问,则通过。通常情况下,系统的登录、退出会配置拦截器。登录的时候,调用subject.login(token),token是用户验证信息,这个时候会在Realm中doGetAuthenticationInfo方法中进行认证。这个时候会把用户提交的验证信息与数据库中存储的认证信息进行比较,一致则允许访问,并在浏览器种下此次回话的cookie,在服务器端存储session信息。退出的时候,调用subject.logout(),会清除回话信息。
shiro中核心概念介绍: Filter:
1.AnonymousFilter:通过此filter修饰的url,任何人都可以进行访问,即使没有进行权限认证
2.FormAuthenticationFilter:通过此filter修饰的url,会对请求的url进行验证,如果没有通过,则会重定向返回到loginurl
3.BasicHttpAuthenticationFilter:通过此filter修饰的url,要求用户已经通过认证,如果没有通过,则会要求通过Authorization 信息进行认证
4.LogoutFilter:通过此filter修饰的url,一旦收到url请求,则会立即调用subject进行退出,并重定向到redirectUrl
5.NoSessionCreationFilter:通过此filter修饰的url,不会创建任何会话
6.PermissionAuthorizationFilter:权限拦截器,验证用户是否具有相关权限
7.PortFilter:端口拦截器,不是通过制定端口访问url,将自动将端口重定向到指定端口
8.HttpMethodPermissionFilter:rest风格拦截器,配置rest的访问方式
9.RolesAuthorizationFilter:角色拦截器,未登陆,将跳转到loginurl,未授权,将跳转到unauthorizedUrl
10.SslFilter:HTTPS拦截器,需要以HTTPS的方式进行访问
11.UserFilter:用户拦截器,需要用户已经认证,或已经remember me
拦截配置说明:
anon:例子/admins/**=anon 没有参数,表示可以匿名使用。
authc:例如/admins/user/**=authc表示需要认证(登录)才能使用,没有参数
roles:例子/admins/user/**=roles[admin],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,例如admins/user/**=roles["admin,guest"],每个参数通过才算通过,相当于hasAllRoles()方法。
perms:例子/admins/user/**=perms[user:add:*],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/admins/user/**=perms["user:add:*,user:modify:*"],当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。
rest:例子/admins/user/**=rest[user],根据请求的方法,相当于/admins/user/**=perms[user:method] ,其中method为post,get,delete等。
port:例子/admins/user/**=port[8081],当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,queryString是你访问的url里的?后面的参数。
authcBasic:例如/admins/user/**=authcBasic没有参数表示httpBasic认证
ssl:例子/admins/user/**=ssl没有参数,表示安全的url请求,协议为https
user:例如/admins/user/**=user没有参数表示必须存在用户,当登入操作时不做检查
注:anon,authcBasic,auchc,user是认证过滤器,
perms,roles,ssl,rest,port是授权过滤器
复制代码
shiro 的权限控制只是做到资源的权限控制,要想实现业务数据的权限控制,肯定是需要耦合到我们具体的业务代码里面的,后面有时间在分享一下现在自己公司的一种解决思路。
上面简单介绍了一下shiro,接下来就进入正题。
加入需要的依赖
<!-- spring-data-jpa -->
<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>
<!-- shiro 相关包 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.0</version>
</dependency>
<!-- shiro+redis缓存插件 -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>2.4.2.1-RELEASE</version>
</dependency>
<!--shiro与thymelef的集成插件-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
复制代码
yml文件在加入一点配置,这里要注意的是jpa 和之前的datasource 处于同一级
#通过jpa 生成数据库表
spring:
jpa:
hibernate:
ddl-auto: update
show-sql: true
thymeleaf:
cache: false
prefix: classpath:/templates/
suffix: .html
encoding: UTF-8
content-type: text/html
mode: HTML5
复制代码
数据库设计 一般的权限管理都会设计到这五张表(用户表、角色表、用户角色中间表、权限表、角色权限中间表)
1、用户表:
@Entity //实体类的注解
@Table(name="sys_user")
public class SysUser implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String userName;
private String passWord;
private int userEnable;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassWord() {
return passWord;
}
public void setPassWord(String passWord) {
this.passWord = passWord;
}
public int getUserEnable() {
return userEnable;
}
public void setUserEnable(int userEnable) {
this.userEnable = userEnable;
}
}
复制代码
2、角色表
@Entity //实体类的注解
@Table(name="sys_role")
public class SysRole implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String roleName;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
}
复制代码
3、用户角色中间表
@Entity //实体类的注解
@Table(name="sys_user_role")
public class SysUserRole implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private int userId;
private int roleId;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
public int getRoleId() {
return roleId;
}
public void setRoleId(int roleId) {
this.roleId = roleId;
}
}
复制代码
4、权限表
@Entity //实体类的注解
@Table(name="sys_permission")
public class SysPermission implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String userName;
private String resUrl;
private String userType;
private String parentId;
private String userSort;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getResUrl() {
return resUrl;
}
public void setResUrl(String resUrl) {
this.resUrl = resUrl;
}
public String getUserType() {
return userType;
}
public void setUserType(String userType) {
this.userType = userType;
}
public String getParentId() {
return parentId;
}
public void setParentId(String parentId) {
this.parentId = parentId;
}
public String getUserSort() {
return userSort;
}
public void setUserSort(String userSort) {
this.userSort = userSort;
}
}
复制代码
5、角色权限中间表
@Entity //实体类的注解
@Table(name="sys_role_permission")
public class SysRolePermission implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private int roleId;
private int permissionId;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getRoleId() {
return roleId;
}
public void setRoleId(int roleId) {
this.roleId = roleId;
}
public int getPermissionId() {
return permissionId;
}
public void setPermissionId(int permissionId) {
this.permissionId = permissionId;
}
}
复制代码
基本上五个表的结构就是这样,有问题到时候再改,实体类建好了,先建数据库也行都一样,现在先建了实体类就通过spring-data-jpa 生成一下数据库的表结构 这时候在重启项目就可以帮我们在数据库建好表了。
shiro 配置类
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 没有登陆的用户只能访问登陆页面
shiroFilterFactoryBean.setLoginUrl("/auth/login");
// 登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/auth/index");
// 未授权界面; ----这个配置了没卵用,具体原因想深入了解的可以自行百度
shiroFilterFactoryBean.setUnauthorizedUrl("/auth/err");
//自定义拦截器
Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>();
shiroFilterFactoryBean.setFilters(filtersMap);
// 权限控制map.
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/img/**", "anon");
filterChainDefinitionMap.put("/auth/login", "anon");
filterChainDefinitionMap.put("/auth/logout", "logout");
filterChainDefinitionMap.put("/auth/kickout", "anon");
//filterChainDefinitionMap.put("/book/**", "authc,perms[book:list],roles[admin]");
//filterChainDefinitionMap.put("/**", "authc,kickout");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置realm.
securityManager.setRealm(myShiroRealm());
// 自定义缓存实现 使用redis
securityManager.setCacheManager(cacheManager());
// 自定义session管理 使用redis
securityManager.setSessionManager(sessionManager());
return securityManager;
}
/**
* 身份认证realm; (这个需要自己写,账号密码校验;权限等)
*
* @return
*/
@Bean
public MyShiroRealm myShiroRealm() {
MyShiroRealm myShiroRealm = new MyShiroRealm();
return myShiroRealm;
}
/**
* cacheManager 缓存 redis实现
* 使用的是shiro-redis开源插件
*
* @return
*/
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
/**
* 配置shiro redisManager
* 使用的是shiro-redis开源插件
*
* @return
*/
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost("localhost");
redisManager.setPort(6379);
redisManager.setExpire(1800);// 配置缓存过期时间
redisManager.setTimeout(0);
// redisManager.setPassword(password);
return redisManager;
}
/**
* Session Manager
* 使用的是shiro-redis开源插件
*/
@Bean
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}
/**
* RedisSessionDAO shiro sessionDao层的实现 通过redis
* 使用的是shiro-redis开源插件
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
/***
* 授权所用配置
*
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
/***
* 使授权注解起作用不如不想配置可以在pom文件中加入
* <dependency>
*<groupId>org.springframework.boot</groupId>
*<artifactId>spring-boot-starter-aop</artifactId>
*</dependency>
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* Shiro生命周期处理器
*
*/
@Bean
public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
}
复制代码
自定义Realm:
public class MyShiroRealm extends AuthorizingRealm {
private static org.slf4j.Logger logger = LoggerFactory.getLogger(MyShiroRealm.class);
//如果项目中用到了事物,@Autowired注解会使事物失效,可以自己用get方法获取值
@Autowired
private SysRoleService roleService;
@Autowired
private UserService userService;
/**
* 认证信息.(身份验证) : Authentication 是用来验证用户身份
*
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {
logger.info("---------------- 执行 Shiro 凭证认证 ----------------------");
UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
String name = token.getUsername();
String password = String.valueOf(token.getPassword());
SysUser user = new SysUser();
user.setUserName(name);
user.setPassWord(password);
// 从数据库获取对应用户名密码的用户
SysUser userList = userService.getUser(user);
if (userList != null) {
// 用户为禁用状态
if (userList.getUserEnable() != 1) {
throw new DisabledAccountException();
}
logger.info("---------------- Shiro 凭证认证成功 ----------------------");
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
userList, //用户
userList.getPassWord(), //密码
getName() //realm name
);
return authenticationInfo;
}
throw new UnknownAccountException();
}
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
logger.info("---------------- 执行 Shiro 权限获取 ---------------------");
Object principal = principals.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
if (principal instanceof SysUser) {
SysUser userLogin = (SysUser) principal;
Set<String> roles = roleService.findRoleNameByUserId(userLogin.getId());
authorizationInfo.addRoles(roles);
Set<String> permissions = userService.findPermissionsByUserId(userLogin.getId());
authorizationInfo.addStringPermissions(permissions);
}
logger.info("---- 获取到以下权限 ----");
logger.info(authorizationInfo.getStringPermissions().toString());
logger.info("---------------- Shiro 权限获取成功 ----------------------");
return authorizationInfo;
}
/**
* 清除所有用户授权信息缓存.
*/
public void clearCachedAuthorizationInfo(String principal) {
SimplePrincipalCollection principals = new SimplePrincipalCollection(principal, getName());
clearCachedAuthorizationInfo(principals);
}
/**
* 清除所有用户授权信息缓存.
*/
public void clearAllCachedAuthorizationInfo() {
Cache<Object, AuthorizationInfo> cache = getAuthorizationCache();
if (cache != null) {
for (Object key : cache.keys()) {
cache.remove(key);
}
}
}
/**
* @Description: TODO 清楚缓存的授权信息
* @return void 返回类型
*/
public void clearAuthz(){
this.clearCachedAuthorizationInfo(SecurityUtils.getSubject().getPrincipals());
}
}
复制代码
在数据库里面插入几条数据开始测试。
就拿之前的代码做测试了,先在配置里配置好需要做权限过滤的路径,和权限规则
这样设置应该是必须登录才能访问,浏览器直接访问一下
结果发现跳转到之前配置的登录页了,就是说这个权限起作用了,把它改成anon,重启试一下 ,其实在启动的控制台我们能看到shiroFilter这个过滤器的信息,过滤的是 /*在访问一下之前的链接,发现可以正常访问到,查到了之前的测试数据
改个角色的权限试试在,之前我们已经给用户设置了 ‘admin’ ‘test’两个角色 , 没有设置‘demo’这个角色,请求应该也会被拦截
果然又跳到了登录页面,把‘demo’ 去掉,发现可以正常请求查到了数据
其实除了在shiro的配置文件配置过滤规则,也可以通过注解的方式在controller上加入权限,效果是一样的
图中框起来的地方可以设置 AND 或者是 OR , 就是设置多个角色的时候, 是全部满足还是满足一个即可
基于角色的权限设置粒度还比较粗,可以在细一点,针对每个功能进行设置,这时候就用到了那张权限表
还用刚才的做测试,设置两个权限,我们在数据库设置的权限是 ‘book:*’ , 测试发现没问题可以请求到
其实除了这种配置方式,还可以通过注解的方式,配置方式类似角色的设置
这样权限配置就差不多了,还有种情况就是要对页面中按钮之类的进行权限控制,做法其实也比较简单
在thymelef 下使用html 进行测试,需要的jar 上面已经导入了,在shiro的config中配置ShiroDialect ,这个上面的配置文件也已经配置好了,剩下的就是在页面头部中引入xmlns
<html lang="zh_CN" xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
复制代码
页面里放两个按钮,配置不同的权限,数据库中的权限改为"book:list",看一下效果
<tr>
<td colspan="2">
<button shiro:hasPermission="book:list" type="reset">
重置
</button>
<button shiro:hasPermission="book:add" type="button" onclick="submit1()">
提交
</button>
</td>
</tr>
复制代码
发现只有拥有权限的按钮才能显示出来,而且查看页面源码发现没有权限的按钮根本就没有生成在页面中
总结一下,了解了上面的这一系列概念和配置,shiro的基本使用应该是没啥问题的了,接下来在研究一下shiro怎么做单点登录的。