1.JavaWeb中的权限控制
(1)什么是权限控制
- 忽略特别细的概念,比如权限能细分很多种,功能权限,数据权限,管理权限等
- 理解两个概念:用户和资源,让指定的用户,只能操作指定的资源(CRUD)
(2)javaweb中怎么处理
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws Exception { HttpServletRequest httpRequest=(HttpServletRequest)request; HttpServletResponse httpResponse=(HttpServletResponse)response; HttpSession session=httpRequest.getSession(); if(session.getAttribute("username")!=null){ chain.doFilter(request, response); } else { httpResponse.sendRedirect(httpRequest.getContextPath()+"/login.jsp"); } }
2.权限框架核心知识ACL和RBAC
2.1.ACL和RBAC简介
- ACL:Access Control List 访问控制列表
- 以前盛行的一种权限设计,它的核心在于用户直接和权限挂钩
- 优点:简单易用、开发便捷
- 缺点:用户和权限直接挂钩,导致在授权时的复杂性,比较分散,不便于管理
- 案例:常见的文件系统权限设计,直接给用户加权,类似Linux系统中的chmod
RBAC:Role Based Access Control
- 基于角色的访问控制系统。权限与角色相关联,用户通过适当的角色的成员而获得角色的权限
- 优点:简化了用户与权限的管理,
- 通过对用户进行分类,使得角色与权限关联起来
- 缺点:开发相比ACL复杂
- 案例:基于RBAC模型的权限验证框架有Apache Shiro、Spring Security
- 总结:权限设计不能太过于复杂,否则性能会下降
2.2主流权限框架介绍
(1)什么是Spring Security
Spring Security是一个能够基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI和AOP功能,为应用系统提供声明式的安全访问控制功能,减少了企业系统安全控制编写大量重复代码的工作。
(2)什么是Apache Shiro
Apache Shiro是一个强大且易用的java安全框架,执行身份验证、授权、密码和会话管理。使用shiro的易于理解的API,可以快速、轻松的执行任何应用程序。
两个的优缺点
- Apache Shiro比Spring Security使用更简单
- Shiro功能强大、简洁、灵活,不跟任何的框架或者容器绑定,可以独立运行
- SpringSecurity对Spring体系支持比较友好,脱离Spring体系开发很难
- SpringSecuity支持Oauth鉴权,Shiro需要自己实现
3.Shiro架构和基本概念
3.1.Shiro的4大核心模块
(1)Shiro的四大核心模块分为身份认证、授权、会话管理和加密
- 身份认证
- Authentication,身份认证,一般就是登录
- 授权
- Authorization,给用户分配角色或者访问某些资源的权限
- 会话管理
- Session Management,用户的会话管理员,多数情况下是web Session
- 加密
- Cryptogarphy,数据加解密,你如密码加解密等
(2)Shiro架构图
3.2.Shiro权限控制运行流程
(1)Shiro常见名称
- Subject
- 我们把用户或者程序称为主体,主体去访问资源或者系统
- SecurityManager
- 安全管理器,Subject的认证和授权都在安全管理器下进行
- Authenticator
- 认证器,主要负责Subject的认证
- Realm
- 数据域,Shiro和安全数据的连接器,好比jdbc连接数据库;通过realm获取认证授权的相关信息
- Authorizer
- 授权器,主要负责Subject的授权,控制subject拥有的角色或者权限
- Cryptography
- 加解密,Shiro包含易于使用和理解的数据加密方法,简化了很多复杂的API
- CacheManager
- 缓存管理器,比如认证或者授权信息,通过缓存进行管理,提高性能
- SessionManager
- 会话管理器,大多数是web session
- SessionDAO
- SessionDAO即会话,是对session会话的一套接口,比如要将session存储到数据库。
4.Shiro简单API案例
4.1.项目搭建所需依赖
- 环境准备:maven3.5+jdk8+springboot+idea
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--mysql starter 注意一定要把runtime去掉--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--测试模块starter--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--阿里巴巴数据源--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.6</version> </dependency> <!--shiro相关依赖包--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency>
4.2.Shiro认证简单实操
(1)Shiro的认证流程
- 创建Security Manager:Security Manager是用来提供安全服务的,所以在做shiro认证的时候要先创建此对象
- 主题Subject提交请求给Security Manager
- Security Manager调用Authenticator组件做认证
- Authenticator通过Realm来从数据源中获取认证数据
(2)编码测试
@SpringBootTest public class Test{ //声明SecurityManager DefaultSecurityManager securityManager = new DefaultSecurityManager(); //声明Realm SimpleAccountRealm accountRealm = new SimpleAccountRealm(); @BeforeTest public void init(){ accountRealm.addAccount("lixiang","123456"); accountRealm.addAccount("lisi","123456"); //构建环境 securityManager.setRealm(accountRealm); } @Test public void test(){ SecurityUtils.setSecurityManager(securityManager); Subject subject = SecurityUtils.getSubject(); UsernameAndPasswordToken token = new UsernameAndPasswordToken("lixiang","123456"); subject.login(token); System.out.println("认证结果:"+subject.isAuthenticated()); } }
(3)测试结果
4.3.Shiro授权简单实操
(1)常用API
//是否有对应角色 subject.hasRole("root") //获取subject名 subject.getPrincipal() //检查是否有对应的角色,无返回值,直接在SecurityManager里面进行判断 subject.checkRole("admin") //检查是否有对应的角色 subject.hasRole("admin") //退出登录 subject.logout();
(2)编码实操
@Test void contextLoads() { SecurityUtils.setSecurityManager(securityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("lixiang","123456"); subject.login(usernamePasswordToken); System.out.println("认证结果:"+subject.isAuthenticated()); System.out.println("获取subject主体的唯一标识:"+subject.getPrincipal()); //检查是否有对应角色,无返回值,直接在SecurityManager里面进行判断 subject.checkRole("admin"); //检查是否有对应的角色 System.out.println("是否有对应角色:"+subject.hasRole("admin")); //退出登录 subject.logout(); System.out.println("认证结果:"+subject.isAuthenticated()); }
5.安全数据来源Realm
5.1.Realm简介和继承关系
- Realm的作用:Shiro从Realm中获取安全的数据
- Realm中的两个概念:
- principal:主体的标识,可以有多个,但是必须要有一个唯一性的,常见的用户名、手机号、邮箱
- credential:访问凭证,一般就是密码
- 如果要自定义Realm,继承AuthorizingRealm
- Realm:顶级接口,所有类的父接口
- CachingRelam:带有缓存功能的Realm抽象类
- AuthenticatingRealm:带有认证功能的Realm抽象类
- AuthorizingRealm:带有授权功能的Realm抽象类
- SimpleAccountRealm:提供一些简单的Realm认证
- TextConfigurationRealm:提供文本形式的Realm认证
- IniRealm和PropertiesRealm:TextConfigurationRealm的子类,细化文本验证方式
- JdbcRealm:与数据库交互的Realm认证
- DefaultLdapRealm:根据LDAP进行身份验证
5.2.Shiro内置IniRealm权限验证
(1)新建shiro.ini文本文件,编写规则
#用户模块,对应用户名、密码、角色,多个角色之间用逗号隔开 [users] lixiang = 123456,user zhangsan = 123456,admin,root #权限模块,对应角色名称、对应权限,多个权限用,分隔 [roles] user = video:find,video:buy admin = video:* root = *
(2)测试编码
@Test public void test(){ //创建IniSecurityManagerFactory工厂实例,注意这块一定要是shiro下的包 //IniSecurityManagerFactory这个类已经废弃了,这里只做验证 Factory<SecurityManager> factory = new IniSecurityManagerFactory(); //获取工厂实例 SecurityManager securityManager = factory.getInstance(); //将securityManager设置到当前运行环境当中 SecurityUtils.setSecurityManager(securityManager); //获取Subject对象 Subject subject = SecurityUtils.getSubject(); //创建登录Token UsernameAndPasswordToken token = new UsernameAndPasswordToken("lixiang","123456"); //验证 subject.login(token); //判断是否有对应角色 System.out.print("判断是否有对应角色:"+subject.hasRole("admin")); //判断是否有对应的权限 System.out.print("判断是否有对应权限:"+subject.isPermitted("video:find")); //判断是否有对应的权限,无返回值,如果检验不通过则抛出异常 //checkPermission("find:video") }
5.3.Shiro内置JdbcRealm权限验证
(1)配置jdbcrealm.ini文件,注意这块一定要是ANSI格式否则运行会抛错
#注意 文件格式必须为ini,编码为ANSI #声明Realm,指定realm类型 jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm #配置数据源 #dataSource=com.mchange.v2.c3p0.ComboPooledDataSource dataSource=com.alibaba.druid.pool.DruidDataSource # mysql-connector-java 5 用的驱动url是com.mysql.jdbc.Driver,mysql-connector-java6以后用的是com.mysql.cj.jdbc.Driver dataSource.driverClassName=com.mysql.cj.jdbc.Driver #避免安全警告 dataSource.url=jdbc:mysql://120.76.62.13:3606/xdclass_shiro?characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false dataSource.username=test dataSource.password=Xdclasstest #指定数据源 jdbcRealm.dataSource=$dataSource #开启查找权限, 默认是false,不会去查找角色对应的权限,坑!!!!! jdbcRealm.permissionsLookupEnabled=true #指定SecurityManager的Realms实现,设置realms,可以有多个,用逗号隔开 securityManager.realms=$jdbcRealm
- 如果编码不是ANSI格式
(2)验证
配置文件中 jdbcRealm.permissionsLookupEnabled=true 一定要设置成true,默认是false不会去校验角色
@Test void contextLoads() { //创建SecurityManager工厂 Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:jdbcrealm.ini"); //拿到工厂 SecurityManager securityManager = factory.getInstance(); //将securityManager设置到当前运行环境当中 SecurityUtils.setSecurityManager(securityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("jack","123"); subject.login(token); System.out.println("认证结果:"+subject.isAuthenticated()); System.out.println("是否有对应的角色:"+subject.hasRole("user")); //查询是否有权限,无返回值,没有则抛异常 //subject.checkPermission("video:delete"); //查询是否有权限,有返回值 System.out.println(subject.isPermitted("video:delete")); }
5.4.Shiro自定义Realm权限配置
(1)自定义Realm步骤
(1)创建一个类,继承AuthorizingRealm->AuthenticatingRealm->CachingRealm->Realm (2)重写授权方法:doGetAuthorizationInfo(进行权限校验的时候会调用) (3)重写认证方法:doGetAuthenticationInfo(当用户登陆的时候会调用)
(2)对象介绍
- UsernamePasswordToken : 对应就是 shiro的token中有Principal和Credential
- UsernameAndPasswordToken->HostAuthenticationToken->AuthenticationToken
- SimpleAuthorizationInfo:代表用户角色权限信息
- SimpleAuthenticationInfo:代表该用户的认证信息
(3)编写自定义的Realm类
public class CustomRealm extends AuthorizingRealm { //user private final static Map<String,String> userMaps = new HashMap<>(); { userMaps.put("lixiang","123"); userMaps.put("lisi","123"); } //roles - > permission private final static Map<String,Set<String>> permissionMaps = new HashMap<>(); { Set<String> set1 = new HashSet<>(); Set<String> set2 = new HashSet<>(); set1.add("video:find"); set1.add("video:buy"); set2.add("video:add"); set2.add("video:delete"); permissionMaps.put("lixiang",set1); permissionMaps.put("lisi",set2); } //user -> role private final Map<String,Set<String>> roleMap = new HashMap<>(); { Set<String> set1 = new HashSet<>(); Set<String> set2 = new HashSet<>(); set1.add("role1"); set1.add("role2"); set2.add("root"); roleMap.put("jack",set1); roleMap.put("xdclass",set2); } /** * 进行权限验证的时候调用 * @param principals * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { System.out.println("进行权限验证doGetAuthorizationInfo"); String username = principals.getPrimaryPrincipal().toString(); Set<String> permissions = getPermissionsfromDB(username); Set<String> roles = getRolesfromDB(username); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); simpleAuthorizationInfo.setRoles(roles); simpleAuthorizationInfo.setStringPermissions(permissions); return simpleAuthorizationInfo; } /** * 通过用户名查找角色 * @param username * @return */ private Set<String> getRolesfromDB(String username) { return roleMap.get(username); } /** * 通过用户名查找权限 * @param username * @return */ private Set<String> getPermissionsfromDB(String username) { return permissionMaps.get(username); } /** * 进行身份验证的时候调用 * @param token * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println(" 进行身份验证doGetAuthenticationInfo"); String username = token.getPrincipal().toString(); String pwd = getPwdfromDB(username); if("".equals(pwd) || pwd == null){ return null; } SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username,pwd,this.getName()); return simpleAuthenticationInfo; } private String getPwdfromDB(String username) { return userMaps.get(username).toString(); } }
(4)测试
SecurityUtils.setSecurityManager(securityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("lixiang","123"); //登录 subject.login(token); //唯一标识 System.out.println("用户名:"+subject.getPrincipal()); System.out.println("是否有对应的角色:"+subject.hasRole("role1")); System.out.println("是否有对应的权限:"+subject.isPermitted("video:find"));
5.5.Shiro源码认证授权流程
认证流程:
- subject.login(token)
- DelegatingSubject.login(token)
AuthenticatingSecurityManager.authenticate(token)
AbstractAuthenticator.authenticate(token)
ModulearRealmAuthenticator.doAuthenticate(token)
ModulearRealmAuthenticator.doSingleRealmAuthentication(token)
AuthenticatingRealm.getAuthenticationInfo(token)
鉴权流程:
- subject.checkRole(“admin”)
- DelegatingSubject.checkRole()
- AuthorizingSecurityManager.checkRole()
- ModulatRealmAuthorizer.checkRole()
- AuthorizingReaim,hasRole()
- AuthorizingRealm.doGetAuthorizationInfo()