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<Menu> 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<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
 /*
 * 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用来作为缓存)

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

<!-- // 采用本地内存方式缓存 -->
<bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"/>
​
<!-- securityManager安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
 <property name="realm" ref="myRealm"></property>
 <property name="cacheManager" ref="cacheManager"></property>
</bean>

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,651评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,468评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,931评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,218评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,234评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,198评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,084评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,926评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,341评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,563评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,731评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,430评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,036评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,676评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,829评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,743评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,629评论 2 354