(1) shiro详解

参考:开涛的教程(估计是全网最全的,感谢开涛)

目录

1、概念

2、架构

2.1 Authentication认证子系统

2.1.1 实现方法一

2.1.2 实现方法二

2.1.3 实现方法三

2.2 Authorization 授权子系统(访问控制)

2.3 Cryptography 加密子系统

2.3.1 单向hash加密算法

2.3.2 双向经典加密/解密算法

2.3.3 密码加密和密码验证

2.3.4 我们项目中加密的例子

2.4 Session Management会话管理子系统

2.4.1 session


1、概念

要理解shiro,先要理解框架的几个概念:

1) Subject(User): 代表当前登陆或者访问的用户;

2)Principals(username):一般指用户名等,唯一表明Subject身份也就是当前用户身份的东西;

3)Credentials(password):凭证,一般指密码,对当前登陆用户进行验证;

4)Realms:域,一般是指存储用户信息(用户名,密码,权限,角色)的数据库,也就是保存用户权限等信息的数据源

5)SecurityManager:shiro安全管理的顶级对象。它集合或者说调用所有其它相关组件,负责所有安全和权限相关处理过程,就像一个中央集权政府;

2、架构

 

shiro的子系统

名词解释:Authentication/ Authorization

Authentication :认证; 身份验证; 证明

Authorization  :  授权,批准

 

2.1 Authentication认证子系统

认证子系统,就是处理用户登录,验证用户登录。我们前面讲到Subject代表当前用户,而Principal和credential分别就代表用户名和密码。登录认证时,其实就是调用的 Subject.login(AuthenticationToken token)方法,AuthenticationToken是一个接口

登录时就会分别调用它的实现类的 getPrincipal()  getCredentials() 来获得用户名(Principal:主体)和密码(Credentials:凭证)。一般实际中我们传给Subject.login()方法的是UsernamePasswordToken 类的对象

一般我们new一个UsernamePasswordToken的对象:UsernamePasswordToken token = new UsernamePasswordToken("xxxusername", "xxxpassword");, 然后 subject.login(token); 就前去登录。相关代码一般如下:

参考:http://www.cnblogs.com/digdeep/p/4612053.html

@RequestMapping(value="/loginController", method=RequestMethod.POST)
    public String login(String userName, String password, String rememberMe, String type, HttpServletRequest req) {
        String error = null;
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
        if(rememberMe != null && "true".equals(rememberMe))
            token.setRememberMe(true);    // 记住我        
        try {
            subject.login(token);
        } catch (UnknownAccountException | IncorrectCredentialsException e1) {
            error = "用户名或密码错误";
        }catch(ExcessiveAttemptsException e){
            userService.lockAccountByNo(no);     // 锁定账户
            error = "超过了尝试登录的次数,您的账户已经被锁定。";
        }catch (AuthenticationException e) {    // 其他错误
            if(e.getMessage() != null)
                error = "发生错误:" + e.getMessage();
            else
                error = "发生错误,无法登录。";
        }
        // .. ...

Authentication 子系统会将password加密,然后使用username和加密之后的password和从Realm(一般是数据库)中根据usename获得的密码进行比较,相同就登录成功,不相同同就登录失败,或者用户名不存在也登录失败。就这么简单。当然从Realm中根据用户名查找用户的过程是需要我们自己编码实现的。该功能的实现,shiro提供了抽象类 AuthenticatingRealm 专门用于从Realm中获得认证信息。所以我们可以继承 抽象类 AuthenticatingRealm

2.1.1 实现方法一

我们只要实现

就行了,其它的shiro会回调该方法,进行登录认证。而实现该方法就是直接从数据源中根据 AuthenticationToken 获得数据就行了。

2.1.2 实现方法二

我们项目中一般使用的是这一种:可以使用 AuthenticatingRealm 的子类 AuthorizingRealm,它本来是用于权限认证的Realm,但是因为他继承了 AuthenticatingRealm,所以实际上我们只要继承 AuthorizingRealm,然后实现它的抽象方法就行了。同时搞定 登录认证 和 权限认证(访问控制):

public class UserRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String userName = (String)principals.getPrimaryPrincipal();
        User user = userService.getUserByName(userName);
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        authorizationInfo.setRoles(userService.findRolesByUserId(user.getId()));
        authorizationInfo.setStringPermissions(userService.findPermissionsByUserId(user.getId()));
        return authorizationInfo;
    }    
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String userName= (String)token.getPrincipal();
        User user = userService.getUserByName(userName);
        if(user == null) {
            throw new UnknownAccountException();//没找到账户
        }
        if(user.getLocked() == 0) {
            throw new LockedAccountException(); //帐号锁定
        }
        if(user.getLocked() == 2){
            throw new AuthenticationException("account was inactive");
        }
        
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                user.getUserName(), // 用户名
                user.getPassword(), // 密码
                ByteSource.Util.bytes(user.getCredentialsSalt()),    // salt
                getName()  // realm name
        );        
        return authenticationInfo;
    }
    @Override
    public void clearCachedAuthorizationInfo(PrincipalCollection principals) {
        super.clearCachedAuthorizationInfo(principals);
    }
    @Override
    public void clearCachedAuthenticationInfo(PrincipalCollection principals) {
        super.clearCachedAuthenticationInfo(principals);
    }
    @Override
    public void clearCache(PrincipalCollection principals) {
        super.clearCache(principals);
    }
    public void clearAllCachedAuthorizationInfo() {
        getAuthorizationCache().clear();
    }
    public void clearAllCachedAuthenticationInfo() {
        getAuthenticationCache().clear();
    }
    public void clearAllCache() {
        clearAllCachedAuthenticationInfo();
        clearAllCachedAuthorizationInfo();
    }
}

上面的 doGetAuthorizationInfo 方法,会在权限认证也就是访问控制时,被回调而 doGetAuthenticationInfo 方法会在登录认证时被回调,返回的 AuthenticationInfo类型的对象,会和用户登录时输入的 用户名和密码(加密之后的)进行比较,相同则登录成功,反之则登录失败。

2.1.3 实现方法三

其实还有更加简单的方法,因为shiro提供了实现了 AuthorizingRealm 中的抽象方法的子类

比如在数据库环境中,我们就可以直接使用 JdbcRealm,一是可以配置它的相关SQL语句,二是继承它,覆盖它的方法。CasRealm用户单点登录环境。

内容略,详见WEB网页中。

 

2.2 Authorization 授权子系统(访问控制)

调用方式:

Subject.isPermitted/ hasRole/……等涉及到权限的地方会自动调用。
例子代码:
boolean bool = subject.hasRole("admin");

上一节中我们已经介绍了如何获得用户所拥有的权限,在需要判断用户是否有某权限或者角色时,会自动回调方法 doGetAuthorizationInfo 来获得用户的角色和权限,我们只需要在 该方法中从Realm也就是数据库表中获得相关信息。我们先看一下shiro是如何表示角色和权限的,这一点比较重要。(上一节中的代码)

protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String no = (String)principals.getPrimaryPrincipal();
        User user = userService.getUserByNo(no);
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        authorizationInfo.setRoles(userService.findRolesByUserId(user.getId()));
        authorizationInfo.setStringPermissions(userService.findPermissionsByUserId(user.getId()));
        return authorizationInfo;
    }

我们看到 doGetAuthorizationInfo 方法中使用了 SimpleAuthorizationInfo 类封装 Role 和 Permission

源码如下:

以下为shiro角色、权限相关的介绍。我们的项目中没有用这种体系,用自己的一套rbac体系实现的。

1) 两种访问控制方式:

SimpleAuthorizationInfo 封装了角色和权限,其实这也说明了实现“访问控制”两种方式:一是 “基于角色的访问控制是“基于资源的访问控制”。所谓的访问控制,是指对于某个资源,当前用户是否有访问的权限。基于角色的访问控制是一种比较粗粒度的访问控制方式,只要你具有了某个或某几个角色,那么你就可以访问某资源。而基于资源的访问控制,是判断你针对该资源是否有某权限,有才能访问,粒度更细,你是否有某权限,可以根据你有哪些角色,然后改角色有哪些权限来判断的,当然也可以不引入角色的概念,直接判断你是否拥有某些权限。当然两种访问方式可以单独使用,也可以混合使用。比如对于比较简单的权限控制,你可以仅仅只使用基于角色的访问控制,仅仅引入角色表,不需要权限表都可以。混合使用是指,你可以同时要求用户具有某角色并且具有某些权限,才能访问某资源。所以shiro的权限控制时极其灵活的(当然也可以不引入角色表,仅仅引入权限表)。

 

2)权限的字符串表示方式

上面说到 角色 和 权限 都是使用字符串来表示的,其实 shiro 提供了一套比较强大有点复杂的权限字符串表示格式(分为:分割的三个部分):

资源:操作:对象实例ID” 表示:对那个资源的哪个实例可以进行哪些操作,支持通配符。

多个操作需要使用 “,” 逗号分割,而 “*” 放在三个位置上,分别表示:任意资源,任意操作,任意实例。

比如:"user:delete:1" 就表示 对user表的id等于1对应的数据或者对象,可以进行删除操作。其实资源表现实现可以是对象,其实最终是对应到数据库表中的记录。

在比如:"user:update,delete" 就表示 对user表(的任意实例)进行更新和删除操作。"user:update,delete" 其实就等价于 “user:update,delete:*”

所以 shiro 的访问控制可以控制到具体实例,或者说具体哪条数据库记录,也可以在表级别控制如果省略掉 对象实例ID部分,就是在表级别控制

3)权限相关表的设计

1> 如果对于简单的情况,可以只使用“基于角色的访问控制”粗粒度方式,不涉及到权限,仅仅只通过判断是否有某角色来判断访问控制,那么就只需要增加一个角色表(roles) 和 一个角色(roles)和用户(user)的多对多的一个中间表——用户角色表(user_role)。

2> 如果仅仅使用权限来控制访问,那么就可以仅仅只增加一个权限表(priv)和一个用户和权限的多对多的一个中间表——用户权限表(user_priv).

3> 如果既要用到角色,又要用到权限(权限根据角色推算出来),那么就要增加:角色表,用户角色表,权限表,角色权限表。

4> 其实还有一种情况:就是角色和权限没有关系,那么就可以增加:角色表,用户角色表,权限表,用户权限表。不过这种方式不同符合常规。

2.3 Cryptography 加密子系统

shiro提供了很完备而且十分易用的加密解密功能。该子系统分为两个部分:一是基于hash单向加密算法二是基于经典加密解密算法密码是可以解密的出明文的。一般用单向的hash加密算法

2.3.1 单向hash加密算法

shiro提供的单向hash加密算法的相关工具类如下:

我们看到提供了 Md2, Md5, Sha1, Sha256, Sha384, Sha512 等等的hash算法。一般而言Md2/Md5系列的算法已经被证实安全性存在不足。所以一般使用Sha系列的算法

看下源码:

我们看到都是使用 super() 调用父类的方法。根据上面截图中提高的相关类,可以有三种方法来实现密码锁需要的hash加密过程:

  • 直接使用  Sha256Hash/Md5Hash 等类,比如:
String sha256 = new Sha256Hash("admin", "11d23ccf28fc1e8cbab8fea97f101fc1d", 2).toString();

根据Sha256Hash的构造函数,"admin" 为需要加密的密码明文,"11d23ccf28fc1e8cbab8fea97f101fc1d" 为加密需要的salt, 2 是迭代次数,也就是hash次数。最后调用 .toString() 就获得了密文。很简单。

  • 使用 Sha256Hash/Md5Hash 等类 父类 SimpleHash ,比如:
sha1 = new SimpleHash("sha-256", "admin", "11d23ccf28fc1e8cbab8fea97f101fc1d", 2).toString();

看到,我们传入了hash算法的名称 "sha-256", 剩下的参数和 Sha256Hash 的一样。

  • 使用 DefaultHashService 和 HashRequest 二者结合来加密:
DefaultHashService hashService = new DefaultHashService();
//        hashService.setHashAlgorithmName("SHA-256"); 
//        hashService.setPrivateSalt(new SimpleByteSource("123"));
//        hashService.setGeneratePublicSalt(false);
//        hashService.setRandomNumberGenerator(new SecureRandomNumberGenerator()); 
//        hashService.setHashIterations(2); //
        
        HashRequest hashRequest = new HashRequest.Builder()
        .setSource(ByteSource.Util.bytes("admin112358"))
        .setSalt("11d23ccf28fc1e8cbab8fea97f101fc1d")
        .setAlgorithmName("SHA-256")
        .setIterations(2).build();

        System.out.println(hashService.computeHash(hashRequest).toHex());

 

我们看到 HashRequest 类专门提供各种加密需要的参数,密码明文,salt, hash算法,迭代次数。这里有个坑,不要调用DefaultHashService的方法来设置各种加密需要的参数(特别是salt相关的参数),而使用专门的类 HashRequest来提供各种参数,因为使用 DefaultHashService 你是无法设置对 salt 的,也无法获得 salt ,而最终我们是需要将 salt 存放入数据库的,DefaultHashService只能设置 privateSalt, 它hash时最终使用的salt是privateSlat 和 自动生成的 publicSalt,二者合成得到的,合成的结果并没有提供方法来使我们获得它。另外DefaultHashService有一个坑:如果你调用方法hashService.setPrivateSalt(new SimpleByteSource("123"));设置了privateSalt, 即使你调用了hashService.setGeneratePublicSalt(false);方法,它还是会随机生成publicSalt的。另外 HashRequest 中提供的参数会覆盖DefaultHashService设置的相应参数。

相比较而言,肯定是直接使用 Sha256Hash/Md5Hash 等类来得最简单而直接。

注解:
所谓加Salt,就是加点“佐料”。当用户首次提供密码时(通常是注册时),
由系统自动往这个密码里加一些“Salt值”,这个值是由系统随机生成的,
并且只有系统知道。然后再散列。而当用户登录时,系统为用户提供的代
码撒上同样的“Salt值”,然后散列,再比较散列值,已确定密码是否正确。   

这样,即便两个用户使用了同一个密码,由于系统为它们生成的salt值不
同,他们的散列值也是不同的。即便黑客可以通过自己的密码和自己生成
的散列值来找具有特定密码的用户,但这个几率太小了(密码和salt值都
得和黑客使用的一样才行)。

2.3.2 双向经典加密/解密算法

略,预知详情请参考web文档。

 

 

2.3.3 密码加密和密码验证

注册时一般涉及到密码加密,登录时涉及到密码验证。通过上面介绍的 加密算法,完全可以自己实现密码加密和密码验证。但是其实shiro也提供了相应的类:

DefaultPasswordService 和 HashedCredentialsMatcher。虽然提供了,其实  DefaultPasswordService 卵用都没有,因为他没有提供获取或者设置 salt 的方法,而 salt 是我们需要存入数据库的(我们程序暂时没这么做,直接把username作为salt的)。所以密码加密我们是不使用 DefaultPasswordService 的,而是根据前面的介绍自己写。至于密码验证我们应该继承 HashedCredentialsMatcher,然后重写它的 doCredentialsMatch() 方法即可:

public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {
    private Cache<String, AtomicInteger> passwordRetryCache;
    public RetryLimitHashedCredentialsMatcher(CacheManager cacheManager) {
        passwordRetryCache = cacheManager.getCache("passwordRetryCache");
    }
    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        String username = (String)token.getPrincipal();
        AtomicInteger retryCount = passwordRetryCache.get(username);
        if(retryCount == null) {
            retryCount = new AtomicInteger(0);
            passwordRetryCache.put(username, retryCount);
        }
        if(retryCount.incrementAndGet() > 5) {
            throw new ExcessiveAttemptsException("超过了尝试登录的次数,您的账户已经被锁定。");
        }
        boolean matches = super.doCredentialsMatch(token, info);
        if(matches) {
            passwordRetryCache.remove(username);
        }
        return matches;
    }
}

 

一下为源文件解释:

super.doCredentialsMatch(token, info)调用了父类的方法:

我们看到 AuthenticationToken token 加密时需要的 salt 来自于 AuthenticationInfo info:

AuthenticationToken token 是登录时页面传过来的用户名,明文密码等参数,AuthenticationInfo info却是从数据库中获得的用户名密码密文,salt等参数。equals(tokenHashedCredentials, accountCredentials); 验证: 明文密码使用相同的salt加密之后,获得的密文是否和数据库中的密码密文一致。一致,则密码验证通过。

2.3.4 我们项目中加密的例子

参考:http://www.cnblogs.com/zr520/archive/2015/12/01/5009790.html

Demo代码:

@Override
	public void AddUser(User user) {
		RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
		String algorithmName = "md5";
		int hashIterations = 2;

		String salt = user.getUsername();

		SimpleHash hash = new SimpleHash(algorithmName, user.getPassword(), salt, hashIterations);
		user.setPassword(hash.toHex());
		userMapper.insert(user);

	}

2.4 Session Management会话管理子系统

shiro中session的最大不同时,它可以使用再非web环境中。对于JavaSE的环境也可以使用session的功能,因为他实现了一套不依赖于web容器的session机制。shiro提供了三个默认的实现:

  • DefaultSessionManager: DefaultSecurityManager使用的默认实现,用于JavaSE环境;
  • ServletContainerSessionManager: DefaultWebSecurityManager使用的默认实现,用于web环境,其直接使用Servlet容器的会话;
  • DefaultWebSessionManager: 用于web环境的实现,可以替代ServletContainerSessionManager,自己维护会话,直接替代Servlet容器的会话管理;

在web环境中默认使用的是 ServletContainerSessionManager

2.4.1 session

会话 :
所谓会话,即用户访问应用时保持的连接关系,在多次交互中应用能够识别出当前访问的用户是谁,且可以在多次交互中保存一些数据。如访问一些网站时登录成功后,网站可以记住用户,且在退出之前都可以识别当前用户是谁。

Shiro: 直接使用Shiro的会话管理可以直接替换如Web容器的会话管理。Shiro提供的会话可以用于JavaSE/JavaEE环境,不依赖于任何底层容器,可以独立使用,是完整的会话模块。

登录成功后使用Subject.getSession()即可获取会话;其等价于Subject.getSession(true),即如果当前没有创建Session对象会创建一个

另外可以设置会话的全局过期时间(毫秒为单位),默认30分钟:

自己设:subject.getSession().setTimeout(1000*60*30);

参考:http://blog.csdn.net/qq_32347977/article/details/51084480

 

参考:http://blog.csdn.net/oppoppoppo/article/details/53432202(非常好)

 

参考:http://blog.csdn.net/xh199110/article/details/47029533

我们有一个项目用的是第一种

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值