今天研究一下 常用的安全框架 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>
YAML 文件在加入一点配置, 这里要注意的是 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 怎么做单点登录的.
来源: https://juejin.im/post/5b972dc96fb9a05d1227edea