Shiro 基本操作

Shiro介绍

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。毕竟官网有十分钟入门,足矣可见该框架的上手难度。

Shiro 功能介绍

gongneng.png

从上图可看出来,主要功能有4个,以及支持一些其他特性。

主要功能

Authentication:认证,用于登录

Authorization:授权,判断用户是否拥有资源权限

Session Management:会话,登录后就是一次会话。

Cryptography:加密,用户密码密文存入数据库。

支持的功能

Web Support:支持Web,SE也能用

Caching:缓存

Concurrency:支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去

Testing:提供测试支持

Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问

Remember Me:记住我,

PS: Shiro不会去维护用户、维护权限;这些需要我们自己去设计/提供;然后通过相应的接口注入给Shiro

在使用的时候还有一个很重要的对象叫:Subject ,该对象表示当前访问的用户

Shiro的一些配置

  1. 自定义Realm,需继承AuthorizingRealm。用户认证与授权。
  1. 把Realm交给SecurityManager。统一管理。
  1. 配置 ShiroFilterFactoryBean 把需要拦截的地址和SecurityManager

第一步:写一个Realm,来认证。

AuthorizingRealm 该接口需要重写两个方法。

doGetAuthorizationInfo 授权,访问某一个具体资源判断是否有该权限

doGetAuthenticationInfo 认证,登录认证

public class UserRealm extends AuthorizingRealm {
 @Resource
 private UserInfoService userInfoService;
 @Resource
 private MenuMapper menuMapper;
 /**
 * 认证,你需要通过用户名查数据库,在进行密码对比(内部自动对比,错误会抛异常)
 */
 @Override
 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
 // token 是在Controller层传入进来的
 // getPrincipal 内部代码调用的就是 getUsername()方法
 String username = (String)token.getPrincipal();
 // 通过用户名查询该用户信息
 UserInfo user = userInfoService.login(username);
 if(user != null){
 // 第一个参数存用户信息。这里存的,其他地方可以取出来。存什么,则取什么。
 // 第二个参数查询出来的密码,内部会判断该密码和输入密码是否相同
 // 第三个参数自定义Realm的信息
 return new SimpleAuthenticationInfo(user, user.getPwd(), getName());
 }
 return null;
 }

 /**
 * 授权,写了一个授权判断的时候会自动执行以下代码。
 * 注意如果授权你采用注解的方式,必须在配置中添加两个Bean,否则不会进入该方法判断。
 */
 @Override
 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
 // 该方法就是取的认证时,存的第一个参数。
 UserInfo user = (UserInfo) principals.getPrimaryPrincipal();
 if(user == null){
 return null;
 }
 List menus = menuMapper.selectButtons(user.getUid());
 for (Menu m: menus) {
 info.addStringPermission(m.getUrl());
 }
 return info;
 }

}

第二部写一个配置类把自定义Realm交给SecurityManager

// 申明配置类
@Configuration
public class ShiroConfig {
​
 // 把自定义存入ioc容器中
 @Bean
 public UserRealm initRealm() {
 return new UserRealm();
 }
​
 @Bean
 public SecurityManager initSecurityManager() {
 DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
 // 将自定义Realm注入进去
 manager.setRealm(initRealm());
 // 这个是记住我功能
 manager.setRememberMeManager(cookieRememberMeManager());
 return manager;
 }
​
 @Bean
 public CookieRememberMeManager cookieRememberMeManager() {
 //实例化rememberme管理器
 CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
 //定义Cookie cookie的名字为rememberMe
 SimpleCookie cookie = new SimpleCookie("rememberMe");
 //定义Cookie的有效时间(s)
 cookie.setMaxAge(24 * 60 * 60 * 3);
 //将cookie设置到rememberme管理器中
 cookieRememberMeManager.setCookie(cookie);
 //设置cookie的值的加密密钥(设置用户数据序列化以后采用的加密密钥)
 cookieRememberMeManager.setCipherKey(Base64.decode("6ZmI6I2j5Y+R5aSn5ZOlAA=="));
 return cookieRememberMeManager;
 }
​
 /**
 * 该方法用于过滤请求
 */
 @Bean
 public ShiroFilterFactoryBean shiroFilter() throws UnsupportedEncodingException {
 //实例化Filter工厂
 ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
 //注册securityManager
 shiroFilterFactoryBean.setSecurityManager(initSecurityManager());
 //设置Shiro过滤器过滤规则
 //LinkHashMap是有序的,shiro会根据添加的顺序进行拦截,匹配到过滤器后就执行该过滤器不会在继续向下查找过滤器
 Map filterChainDefinitionMap = new LinkedHashMap();
 /*
 * anon:所有的url都可以不登陆的情况下访问
 * authc:所有url都必须 认证 通过才可以访问
 * user:有记住我才能访问
 * role:拥有某个角色才能访问   filterChainDefinitionMap.put("/user/add", "perms[user:add]");
 * perms:拥有某个资源的权限才能访问
 */
 filterChainDefinitionMap.put("/js/**", "anon");
 filterChainDefinitionMap.put("/css/**", "anon");
 filterChainDefinitionMap.put("/login.html", "anon");
 filterChainDefinitionMap.put("/login", "anon");
 filterChainDefinitionMap.put("/logout", "logout");
 filterChainDefinitionMap.put("/**", "user");
 //未登录时重定向的网页地址
 shiroFilterFactoryBean.setLoginUrl("/login.html");
 // 未授权的时候跳转地址
 shiroFilterFactoryBean.setUnauthorizedUrl("/login.html");
 shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
 return shiroFilterFactoryBean;
 }

 // 这下面两个 Bean 用于开启注解形式的授权验证。(就是开启AOP)
 @Bean
 public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
 AuthorizationAttributeSourceAdvisor advisor=new AuthorizationAttributeSourceAdvisor();
 advisor.setSecurityManager(initSecurityManager());
 return advisor;
 }
 @Bean
 public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
 DefaultAdvisorAutoProxyCreator app=new DefaultAdvisorAutoProxyCreator();
 app.setProxyTargetClass(true);
 return app;
 }
​
}

这个配置的意思是:放行静态资源,登录页面,和登录请求,其他全部请求都被拦截需要对应的权限。

以上就是Shiro 相关的配置就算是写完了。但是是不够的,因为没有调用上面的方法。

@RequestMapping("/login")
public Result login(String username,String pwd,boolean rememberme){
 // 创建一个token,这里的token 就是自定义Realm取出来的token
 UsernamePasswordToken token = new UsernamePasswordToken(username, pwd, rememberme);
 // subject 表示当前访问的用户
 Subject subject = SecurityUtils.getSubject();
 // 判断是否认证过 认证过为true,没认证为false
 if (!subject.isAuthenticated()){
 // 执行登录方法,该方法不是Shiro提供的,但是最终会执行自定义Realm的认证方法上面。
 subject.login(token);
 }
 return new Result("200","ok",null,null);
}

这样一个认证的流程就算走完了。

  1. 自定义Realm
  1. 编写配置类,把Realm注入IOC容器,并且放进SecurityManager
  1. 编写请求过滤,并把SecurityManager放入ShiroFilterFactoryBean对象中
  1. 在登录方法中,把用户名、密码和记住我封装进UsernamePasswordToken对象中。
  1. 使用SecurityUtils获取Subject。
  1. 通过Subject.login(token)进行验证,验证成功则正常执行,验证失败则抛异常。

代码逻辑对比

在没有Shiro的时候代码逻辑是这样的

Controller接受用户名密码,调用Service,查询dao层,结果返回给Service层,Service层进行密码对比并把结果返回给Controller。

有了Shiro是这样的:

Controller接受用户名密码,对用户名密码进行一次封装,接着调用subject的login方法。

login方法,会去自动找到写的Realm(前提继承了AuthorizingRealm),在Realm调用Service,dao层查询,结果层层返回,返回到Realm,对结果集进行再次封装。封装的里面他会自动进行密码的对比。失败则抛异常,成功则返回到Controller层。

图形化说明:

[图片上传失败...(image-a93460-1593264897452)]

授权方式

编码方式授权判断:

Subject subject = SecurityUtils.getSubject();    
if (subject.hasRole("administrator")) {    
 //拥有角色administrator
} else {    
 //没有角色处理
}
if(subject.isPermitted("insert")){
 // 拥有某权限
}else{
 // 没有权限
}

注解方式:(配置文件需要额外配置,上面有说明)

注解 意义 案例
@RequiresAuthentication 验证用户是否登录
@RequiresUser 当前用户已经验证过了或则记住我了
@RequiresGuest 是否是游客身份
@RequiresRoles 拥有该角色 @RequiresRoles({“admin”})
@RequiresPermissions 需要拥有权限 @RequiresPermissions("/development/bug/update")

密码加密

上面的例子当中并没有涉及到密码加密,但是流程都是一毛一样的,唯一区别就是密码不同。

new SimpleHash(algorithmName, source, salt, hashIterations) 用来加密

new SimpleHash("MD5", "123456", "abcd")

四个参数:

  • algorithmName 加密方式写 MD5

  • source 需加密的密码

  • salt 加密的盐

  • hashIterations 加密几次

在自定义Realm里面同样调用 new SimpleAuthenticationInfo方法,只不过参数变成了4个

第一个参数存用户信息。这里存的,其他地方可以取出来。存什么,则取什么
第二个参数查询出来的密码,内部会判断该密码和输入密码是否相同
第三个参数 加密盐,需要通过 ByteSource.Util.bytes(solt)
第四个参数自定义Realm的信息

but !!! 虽然你这样写了。但是!他内部是不会加密滴!

需要配置文件中加入这么一个东东:

@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
 HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
 // 散列算法, 与注册时使用的散列算法相同
 hashedCredentialsMatcher.setHashAlgorithmName("MD5");
 // 散列次数, 与注册时使用的散列册数相同
 hashedCredentialsMatcher.setHashIterations(1);
 // 生成16进制  默认生成的32位
 //hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
 return hashedCredentialsMatcher;
}
// 这么做了还差一点点,要把这个Bean注入 自定义Realm中
 @Bean
 public UserRealm initRealm() {
 UserRealm userRealm = new UserRealm();
 // 注入进去它才会使用,这下就没问题了
 userRealm.setCredentialsMatcher(hashedCredentialsMatcher());
 return userRealm;
 }

最后补充一点

你会发现一个问题,那就是每次授权的时候他都会走查询,意味着每次都要查询数据库,如果访问量大了明显很难受的,所以可以加缓存!

  1. 使用Ehcache(系统混合缓存方案);

  2. 使用本地内存缓存方案;

  3. 自定义CacheManager(比如Redis用来作为缓存)

这里就短暂介绍一下最简单的(方案二)。



 
 

很简单,就算配置成Java代码也是分分钟的事情,这样只会第一次走查询,后面都走缓存了
但是还是有缺点:我在你登录期间修改了你的权限,并不能动态修改。他依旧走的是缓存。
所以其他方案以后在整一整。

你可能感兴趣的:(Shiro 基本操作)