简介:
Apache Shiro提供了认证、授权、加密和会话管理功能,将复杂的问题隐藏起来,提供清晰直观的API使开发者可以很轻松地开发自己的程序安全代码。并且在实现此目标时无须依赖第三方的框架、容器或服务,当然也能做到与这些环境的整合,使其在任何环境下都可拿来使用。
Shiro的四大安全基石:
- 认证(Authentication):用户身份识别,常被看作"登录",是用户证明自己是谁的一个行为。
- 授权(Authorization):访问控制过程,好比决定认证(谁)可以访问什么。
- 会话管理(SessionManagement):管理用户的会话(sessions),管理用户与时间相关的状态。
- 加密(Cryptography):使用加密算法保护数据更加安全,防止数据被偷窥。
其他附加功能:
Web支持:利用Shiro的web支持API可以很容易地实现web程序安全;
Caching:Caching在Apache Shiro的API中是一等公民,确保安全认证的动作快速而有效。
并发(Concurrency):Apache Shiro支持多线程;
测试(Testing):支持测试,帮助你开发单元和综合测试程序确保你的代码如你所预期的那样进行安全认证。
“Run As”:允许用户使用其他用户身份(如果被允许),这在执行某些管理角本中非常有用。
“Remember Me”:在整个会话周期中(sessions)记住用户的身份,用户只需要在程序强制要求登录的情况下才需要登录。
Shiro的三大核心组件:
**1、Subject:**即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。但考虑到大多数目的和用途,你可以把它认为是Shiro的“用户”概念。
**2、SecurityManager:**它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。
3、Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的链接细节,并在需要的时候将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和授权,多个也是可以的。
Shiro内置了可以连接大量安全数据源(又名目录)的Realm,如LDAP、关系数据库(JDBC)、类似INI的文本配置资源以及属性文件等。如果缺省的Realm不能满足需求,你还可以插入代表自定义数据源的自己的Realm实现。
Shiro的执行流程:
一、实现登录验证
对比传统登录方式:
Shiro实现登录:就相当于shiro帮我们解决认证,授权,加密和密码比较的过程。
整合Shiro:
- 添加依赖:
<!--shiro start-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<!--shiro end-->
- 配置过滤器:
相关过滤器有:
anon: 匿名过滤器,未登陆也可以访问
authc: 认证过滤器, 登陆后访问
perms : 需要xx权限,才能访问
roles: 需要xx角色,才能访问
user: 需要xx用户,才能访问
port:指定端口才能访问
ssl:必须使用https协议才能访问
logout :登出功能
rest :根据指定HTTP请求访问才能访问 ,get方式提交 或者 post方式提交才能访问
配置config类(springboot):
shiro的配置步骤
1 配置安全管理器SecurityManager
2 realm域配置:由于SecurityManger需要使用realm域,涉及到用户信息、权限信息,处理用户信息的时候需要加密
3 密码比较器:用户输入的铭文进行加密,并且与数据库中的密文进行比较
4 配置生成过滤器的工厂类
/**
* 在ShiroConfig中做什么事情呢?
* 1 配置shiro安全管理器,向安全管理器中注入Realm域
* 2 配置Realm域:注入密码比较器
* 3 配置密码比较器
* 4 配置拦截路径和放行路径
*/
@Configuration
public class ShiroConfig {
/**
* 配置安全管理器,并且注入Realm域
* @param realm
* @return
*/
@Bean
public SecurityManager securityManager(Realm realm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
return securityManager;
}
/**
* Credentials:凭证/证书 ---
*
* 配置Realm域,注入密码比较器
* @param credentialsMatcher
* @return
*/
@Bean
public BosRealm realm(CredentialsMatcher credentialsMatcher){
BosRealm bosRealm = new BosRealm();
//注入密码比较器,比较用户密码是否和数据库一致
bosRealm.setCredentialsMatcher(credentialsMatcher);
return bosRealm;
}
/**
* 密码比较器
*
* @return
*/
@Bean
public CredentialsMatcher credentialsMatcher(){
// return new HashedCredentialsMatcher("MD5");
return new BosCredentialsMatcher();
}
/**
* 配置拦截路径和放行路径
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager){
System.out.println("ShiroConfiguration.shirFilter()");
// shiro过滤器工厂类
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
//拦截器----Map集合
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();
//配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
filterChainDefinitionMap.put("/login*", "anon");
filterChainDefinitionMap.put("/user/login*", "anon");
filterChainDefinitionMap.put("/validatecode.jsp*", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/images/**", "anon");
filterChainDefinitionMap.put("/data/**", "anon");
// /** 匹配所有的路径
// 通过Map集合组成了一个拦截器链 ,自顶向下过滤,一旦匹配,则不再执行下面的过滤
// 如果下面的定义与上面冲突,那按照了谁先定义谁说了算
// /** 一定要配置在最后
filterChainDefinitionMap.put("/**", "authc");
// 将拦截器链设置到shiro中
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/login.html");
// 登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/index.html");
//未授权界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
return shiroFilterFactoryBean;
}
/**
* 开启shiro aop注解支持
* 使用代理方式;所以需要开启代码支持
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* 开启cglib代理
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
}
创建自定义的Realm类:
//自定义Realm ,实现安全数据 连接
public class BosRealm extends AuthorizingRealm {
@Resource
private UserService userService;
@Resource
private RoleService roleService;
@Resource
private PermissionService permissionService;
@Override
public String getName() {
return "bosRealm";
}
@Override
// 认证...
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
System.out.println("shiro 认证管理... ");
//1 获得密码
String username = (String)token.getPrincipal();
//2 通过username从数据库中查找 User对象,如果找到,没找到.
User user = userService.findUserByUsername(username);;
if(user == null){
//返回null表示账号不存在
return null;
}
return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
}
@Override
// 授权...
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection pc) {
System.out.println("shiro 授权管理...");
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 根据当前登录用户 查询对应角色和权限
// User user = (User) SecurityUtils.getSubject().getPrincipal();
User user = (User) pc.getPrimaryPrincipal();
// 调用业务层,查询角色
List<Role> roles = roleService.findByUser(user);
for (Role role : roles) {
authorizationInfo.addRole(role.getKeyword());
}
// 调用业务层,查询权限
List<Permission> permissions = permissionService.findByUser(user);
for (Permission permission : permissions) {
authorizationInfo.addStringPermission(permission.getKeyword());
}
return authorizationInfo;
}
}
编写加密工具类:
public class Encrypt {
/*
* 散列算法一般用于生成数据的摘要信息,是一种不可逆的算法,一般适合存储密码之类的数据,
* 常见的散列算法如MD5、SHA等。一般进行散列时最好提供一个salt(盐),比如加密密码“admin”,
* 产生的散列值是“21232f297a57a5a743894a0e4a801fc3”,
* 可以到一些md5解密网站很容易的通过散列值得到密码“admin”,
* 即如果直接对密码进行散列相对来说破解更容易,此时我们可以加一些只有系统知道的干扰数据,
* 如用户名和ID(即盐);这样散列的对象是“密码+用户名+ID”,这样生成的散列值相对来说更难破解。
*/
//高强度加密算法,不可逆
public static String md5(String password, String salt){
return new Md5Hash(password,salt,2).toString();
}
public static void main(String[] args) {
/**
* new Md5Hash("123456","lisi",1) :1b539b60601b934441308049a9526e7d
* new Md5Hash("123456","lisi",2) :42bd4e7685cb11d3ba02716c313cb04b
* new Md5Hash("123456","lisi",3) :16f807d62105b4896034552ee5caeb8a
* new Md5Hash("123456","KMNO4",3):8bd35dc14dc07f756478bb44513694f6
*/
//System.out.println(new Md5Hash("123456","KMNO4",3).toString());
/**
* sha家族加密算法
* sha1:aca1eb31d2dcf8f1fcf3fd7a7104232785afad41 40 位
* sha256: 616a47d8e1e42f23693bb3a85749bf18d4b6e5380ddfd5717aafa61e33d5211e
* sha384:84f5cbb18e2d9f1c81b8cec6f443a2b229993689a2ebae97db37e13af1dfb00ec6168713a53fe19d33a63d4d30889553
* sha512:c3e5102b6a7ec6caa5b255dae2895b11c2ef0c7b9bfea8e848653372b53f3ef665d96ea283a21eac683cc0fe5c4b1f64692c2056a8a9636ee1931151043d2b5d
*/
System.out.println("sha1:"+new Sha1Hash("123456","lisi",2));
System.out.println("sha256:"+new Sha256Hash("123456","lisi",2));
System.out.println("sha384:"+new Sha384Hash("123456","lisi",2));
System.out.println("sha512:"+new Sha512Hash("123456","lisi",2));
}
}
编写密码比较器:
public class BosCredentialsMatcher extends SimpleCredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
//向下转型
UsernamePasswordToken upToken = (UsernamePasswordToken)token;
//获取用户页面输入的密码
String pwd = new String(upToken.getPassword());
//加密
String newPwd =Encrypt.md5(pwd, upToken.getUsername()).toString();
//获取数据库密码
String dbPwd = info.getCredentials().toString();
return equals(newPwd, dbPwd);
}
}
二、实现动态菜单
不同角色看到的菜单是不一样的,买家,品牌商,管理员看到的界面是不同的。
- 修改index.html加载基本菜单url路径
// 基本功能菜单加载
$.get("/menu/showMenu",function(data){
$.fn.zTree.init($("#treeMenu"), setting, data);
},"json");
- 在MenuController添加showMenu方法
@RestController
@RequestMapping("/menu")
public class MenuController {
@Autowired
private MenuService menuService;
// 加载左侧的菜单功能
@GetMapping(value = "/showMenu")
public ResponseEntity<List<Menu>> showMenu(){
// 调用业务层,查询当前用户具有菜单列表
Subject subject = SecurityUtils.getSubject();
User user = (User)subject.getPrincipal();
// 查询菜单列表
List<Menu> result = menuService.findByUser(user);
return new ResponseEntity<List<Menu>>(result,HttpStatus.OK);
}
}
- 编写MenuService的业务层
@Service
@Transactional
public class MenuService {
@Autowired
private MenuMapper menuMapper;
/**查询用户*/
public List<Menu> findByUser(User user) {
// 针对admin用户显示所有的菜单
if(user.getUsername().equals("admin")){
return menuMapper.selectAll();
}
else{
// 使用用户ID,查询当前用户具有的菜单列表
return menuMapper.findByUser(user.getId());
}
}
}
- 调用DAO
@org.apache.ibatis.annotations.Mapper
public interface MenuMapper extends Mapper<Menu> {
@Select("select m.* from t_menu m,t_user u,t_user_role ur,t_role r,t_role_menu rm "
+ "where m.id = rm.menu_id and rm.role_id = r.id "
+ "and r.id = ur.role_id and ur.user_id = u.id "
+ "and u.id=#{id} order by m.priority")
List<Menu> findByUser(Integer id);
}
三、细粒度控制方法的权限
对商品的增删查改的方法只能由品牌商这个角色可以调用,通过Shiro提供的若干注解,用于在方法上进行权限控制。
方法注解权限控制:基于代理技术实现
因为我们在前面的shiro的配置类中已经配置了shiro注解框架,使用cglib来创建代理对象,配置shiro aop注解来创建代理,所以这里我们就只要在方法上直接添加以上的注解,就可以实现方法的权限控制了。
无权访问: