Shiro
一. Shiro权限
- 什么是权限控制:
- 忽略特别细的概念,比如权限能细分很多种,功能权限,数据权限,管理权限等
- 理解两个概念:用户和资源,让指定的用户,只能操作指定的资源(CRUD)
- 初学javaweb时怎么做
- Filter接口中有一个doFilter方法,自己编写好业务Filter,并配置对哪个web资源进行拦截后
- 如果访问的路径命中对应的Filter,则会执行doFilter()方法,然后判断是否有权限进行访问对应的资源
- /api/user/info?id=1
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) {
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");
}
}
二. 权限框架ACL和RBAC
2.1 什么是ACL和RBAC
- ACL: Access Control List 访问控制列表
- 以前盛行的一种权限设计,它的核心在于用户直接和权限挂钩
- 优点:简单易用,开发便捷
- 缺点:用户和权限直接挂钩,导致在授予时的复杂性,比较分散,不便于管理
- 例子:常见的文件系统权限设计, 直接给用户加权限
- RBAC: Role Based Access Control
- 基于角色的访问控制系统。权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限
- 优点:简化了用户与权限的管理,通过对用户进行分类,使得角色与权限关联起来
- 缺点:开发对比ACL相对复杂
- 例子:基于RBAC模型的权限验证框架与应用 Apache Shiro、spring Security
- BAT企业 ACL,一般是对报表系统,阿里的ODPS
- 总结:不能过于复杂,规则过多,维护性和性能会下降, 更多分类 ABAC、PBAC等
2.2 主流权限框架介绍和技术选型讲解
什么是 spring Security:官网基础介绍
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
一句话:Spring Security 的前身是 Acegi Security ,是 Spring 项目组中用来提供安全认证服务的框架什么是 Apache Shiro:官网基础介绍
- https://github.com/apache/shiro
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。一句话:Shiro是一个强大易用的Java安全框架,提供了认证、授权、加密和会话管理等功能
两个优缺点,应该怎么选择
- Apache Shiro比Spring Security , 前者使用更简单
- Shiro 功能强大、 简单、灵活, 不跟任何的框架或者容器绑定,可以独立运行
- Spring Security 对Spring 体系支持比较好,脱离Spring体系则很难开发
- SpringSecutiry 支持Oauth鉴权 https://spring.io/projects/spring-security-oauth,Shiro需要自己实现
三. Apache Shiro基础概念和架构
3.1 Shiro核心知识之架构图交互和四大模块
- 直达Apache Shiro官网 http://shiro.apache.org/introduction.html
- 什么是身份认证
- Authentication,身份证认证,一般就是登录
- 什么是授权
- Authorization,给用户分配角色或者访问某些资源的权限
- 什么是会话管理
- Session Management, 用户的会话管理员,多数情况下是web session
- 什么是加密
- Cryptography, 数据加解密,比如密码加解密等(如盐+md5(password))

3.2 Shrio权限控制流程和概念
- Subject
- 我们把用户或者程序称为主体(如用户,第三方服务,cron作业),主体去访问系统或者资源
- SecurityManager
- 安全管理器,Subject的认证和授权都要在安全管理器下进行
- Authenticator
- 认证器,主要负责Subject的认证
- Realm
- 数据域,Shiro和安全数据的连接器,好比jdbc连接数据库; 通过realm获取认证授权相关信息
- Authorizer
- 授权器,主要负责Subject的授权, 控制subject拥有的角色或者权限
- Cryptography
- 加解密,Shiro的包含易于使用和理解的数据加解密方法,简化了很多复杂的api
- Cache Manager
- 缓存管理器,比如认证或授权信息,通过缓存进行管理,提高性能
更多资料导航:http://shiro.apache.org/reference.html
四. Spring boot整合Shiro
4.1 依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP-java7</artifactId>
<version>2.4.13</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
4.2 数据库表的设计
Subject.hasrole()
Subject.ispermitted()
权限校验就是在authenrization中通过数据库查用户信息,然后给subject授权
sys_user

sys_role

sys_user_role

-sys_role_operation_permission

sys_operation_permission

-sys_menu_permission

sys_role_menu_permission

4.3 自定义realm
/**
* Realm:用户所有的认证和授权都是在这里完成的
*/
public class CustomizeRealm extends AuthorizingRealm {
private SysUserMapper sysUserMapper;
public void setSysUserMapper(SysUserMapper sysUserMapper) {
this.sysUserMapper = sysUserMapper;
}
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal();
//查询用户对应的权限
SimpleAuthorizationInfo ai = new SimpleAuthorizationInfo();
Set<String> permission = new HashSet<>();
//给予权限
permission.add("user#list");
ai.setStringPermissions(permission);
return ai;
}
//用户的认证在这里完成
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
if (token instanceof UsernamePasswordToken){
UsernamePasswordToken upt = (UsernamePasswordToken) token;
String username = upt.getUsername();
//char[] password = upt.getPassword();
QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username",username);
//根据用户名在数据库中查询到对应的用户
SysUser sysUser = sysUserMapper.selectOne(queryWrapper);
//表示没有对应的用户
if (null == sysUser){
//直接抛出异常,会在LoginController中捕获
throw new UnknownAccountException(String.format("<%s>不存在",username));
}else {
//根据用户名查询有该用户,然后接着要校验密码
//密码的校验这个工作不是我们来做的
/**
* SimpleAuthenticationInfo 构造方法接收三个参数
* 1.第一个是用户名
* 2.第二个是数据库的密码
* 3.第三个是用户的真实的姓名
* shiro底层会自己比对token中密码和数据库密码
*/
SimpleAuthenticationInfo sai = new SimpleAuthenticationInfo(username,sysUser.getPassword(),username);//进入CustomizeCredentialsMatcher
return sai;
}
}
System.out.println(token);
return null;
}
}
SecurityUtils.getSubject().getPrincipal()获取的用户信息是在哪里设置的?
自定义realm–》doGetAuthenticationInfo–》SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), byteSourceSalt, getName());
方法第一个参数,这里设置的就是SecurityUtils.getSubject().getPrincipal()获取的值,可以设置String和对象。
以前一直分不清 authentication(认证) 和 authorization(授权),其实很简单,举个例子来说:
你要登机,你需要出示你的身份证和机票,身份证是为了证明你张三确实是你张三,这就是 authentication;而机票是为了证明你张三确实买了票可以上飞机,这就是 authorization。
4.3 shiro配置
@Configuration
public class ShiroConfig {
private SysUserMapper sysUserMapper;
public ShiroConfig(SysUserMapper sysUserMapper) {
this.sysUserMapper = sysUserMapper;
}
//Shiro框架的入口
@Bean
public ShiroFilterFactoryBean filterFactoryBean(){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager());
//用户未登录的时候访问的接口
shiroFilterFactoryBean.setLoginUrl("/login/un");
//loginUrl:login.jsp
//shiroFilterFactoryBean.setLoginUrl("login.html");
Map<String,String> filterChain = new HashMap<>();
//表示这个uri是用户未登录的场景下是可以访问的
//filterChain.put("/login.html","anon");
filterChain.put("/login","anon"); //anonymous匿名访问
filterChain.put("/login/un","anon");
//表示访问 /user/**,需要登录才能访问
filterChain.put("/**","authc");//authentication
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChain);
return shiroFilterFactoryBean;
}
//安全管理器
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm());
securityManager.setSessionManager(new CustomizeSessionManager());
return securityManager;
}
//这是Realm,SysUserMapper会自动的注入进来
@Bean
public Realm realm(){
CustomizeRealm customizeRealm = new CustomizeRealm();
customizeRealm.setSysUserMapper(sysUserMapper);
//setCredentialsMatcher() 设置用户自定义的密码比较方式
customizeRealm.setCredentialsMatcher(new CustomizeCredentialsMatcher());
return customizeRealm;
}
/**
* 该类的作用是,实现注解的方式来设置权限
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisor = new DefaultAdvisorAutoProxyCreator();
advisor.setProxyTargetClass(true);
return advisor;
}
/**
* 实现注解的方式来配置权限
*/
@Bean
public AuthorizationAttributeSourceAdvisor
authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new
AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
4.4 用户登录
@RequestMapping("/login")
@RestController
public class LoginController {
@GetMapping
public Object login(String username, String password, HttpSession session){
//获取一个主题,将来用于封装用户的用户信息(登录成功后),该subject,shiro会自动管理
Subject subject = SecurityUtils.getSubject();
AuthenticationToken token = new UsernamePasswordToken(username,password);
Map<String,Object> result = new HashMap<>();
try {
//执行登录(失败就报异常)
subject.login(token);
result.put("code",1);
result.put("msg","success");
result.put("token",session.getId());
return result;
} catch (AuthenticationException e) {
e.printStackTrace();
result.put("code",-1);
result.put("msg","用户名或密码错误");
result.put("token",session.getId());
}
return result;
}
@RequestMapping("/un")
public Object unLogin(){
Map<String,Object> result = new HashMap<>();
result.put("code",-1);
result.put("msg","你还未登录,请登录");
return result;
}
}
4.5 自定义SessionManager
/**
* 在前后端分离的场景下,session如何与前端建立联系
* 只需要在getSessionId()方法中返回sessionId
*/
public class CustomizeSessionManager extends DefaultWebSessionManager {
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
//return super.getSessionId(request, response);针对前后端不分离的情况
// String sessionId = WebUtils.getHttpRequest(request).getHeader("Authorization");
// 如果从请求头中能获取到sessionId,需要将其余shiro内部管理的session进行关联,然后再返回
ShiroHttpServletRequest req = (ShiroHttpServletRequest)request;
String sessionId = req.getHeader("Authorization");
if (null != sessionId){
//这里的代码如何写?进入到super.getSessionId()中去,然后进入getReferenceSessionId()这个方法中
//将所有的if(id != null) 这个判断下的代码全部拷贝过来
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId);
//automatically mark it valid here. If it is invalid, the
//onUnknownSession method below will be invoked and we'll remove the attribute at that time.
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return sessionId;
}else {
return super.getSessionId(request, response);
}
}
}
4.6 自定义CredentialsMatcher
/**
* 用户自定义的密码比较方式
*/
@Slf4j
public class CustomizeCredentialsMatcher implements CredentialsMatcher {
private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
/**
*当用户在Realm中设置了CredentialsMatcher这个之后,那么SimpleAuthenticationInfo
* 中方式数据库密码和UsernamePasswordToken中放置的用户原始密码,比较的时候会自动的进入这个方法
*
* token -> 就是LoginController中UsernamePasswordToken
* info -> 就是CustomizeRealm中doGetAuthenticationInfo的返回值
*
* 返回值:tr
*/
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
log.info("密码比对");
char[] rawPassword = (char[]) token.getCredentials();
String encodePassword = (String) info.getCredentials();
return passwordEncoder.matches(String.copyValueOf(rawPassword),encodePassword);
}
}
五. 权限数据缓存
redis作为企业使用最为频繁的中间件,用来缓存各种业务数据,在使用shiro的缓存的时候,课程中还是采用redis来作为缓存中间件。下载地址:https://github.com/MicrosoftArchive/redis/releases
5.1 引入依赖
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.2.3</version>
</dependency>
5.2 配置RedisManager
@Bean
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost("localhost:6379");
return redisManager;
}
5.3 配置CacheManager
@Bean
public CacheManager cacheManager(RedisManager redisManager) {
RedisCacheManager cacheManager = new RedisCacheManager();
cacheManager.setRedisManager(redisManager);
return cacheManager;
}
5.4 在SecurityManager中加入缓存管理
securityManager.setCacheManager(cacheManager);
六. Session数据的缓存
6.1 配置RedisSessionDao
@Bean
public RedisSessionDAO redisSessionDAO(RedisManager redisManager) {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager);
return redisSessionDAO;
}
6.2 SessionManager的设置
@Bean
public SessionManager sessionManager(RedisSessionDAO redisSessionDAO){
CustomSessionManager sessionManager = new CustomSessionManager();
sessionManager.setSessionDAO(redisSessionDAO);
return sessionManager;
}
附录:
/**
shiro内部提供了做两次md5处理。但是得到数据与其他工具类,得到两次md5数据不一致。
*/
@Bean
public CredentialsMatcher credentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("MD5"); //设置加密方式
hashedCredentialsMatcher.setHashIterations(2); //作两次md5加密
return hashedCredentialsMatcher;
}
处理方式:在用户注册的时候,存入密码的时候就按照shiro的规则来储存密码。
new SimpleHash("md5", "123", null, 2).toString() // 将这种处理方式得到的密码存入数据库。