shiro入门学习四

INI配置解析

从之前的shiro架构图可以看出,shiro是从根对象SecurityManager进行身份验证和授权的,这个对象是线程安全且整个应用只需要一个即可,因此Shiro提供了SecurityUtils让我们绑定它为全局的,shiro的类都是POJO的,很容易放到任何IOC容器管理,shiro提供的INI配置类似于Spring之类的IOC/DI容器,shiro支持的依赖注入:public空参构造器对象的创建、setter依赖注入。

纯java代码写法

public void pureJava(){
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        //设置authenticator
        ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
        authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
        securityManager.setAuthenticator(authenticator);

        //设置authorizer
        ModularRealmAuthorizer authorizer = new ModularRealmAuthorizer();
        authorizer.setPermissionResolver(new WildcardPermissionResolver());
        securityManager.setAuthorizer(authorizer);

        //设置Realm
        DruidDataSource ds = new DruidDataSource();
        ds.setDriverClassName("com.mysql.jdbc.Driver");
        ds.setUrl("jdbc:mysql://localhost:3306/shiro");
        ds.setName("root");
        ds.setPassword("");
        JdbcRealm jdbcRealm = new JdbcRealm();
        jdbcRealm.setDataSource(ds);
        jdbcRealm.setPermissionsLookupEnabled(true);
        securityManager.setRealms(Arrays.asList((Realm)jdbcRealm));

        //将SecurityManager设置到SecurityUtils中
        SecurityUtils.setSecurityManager(securityManager);
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken("zhang","123");
        subject.login(token);
        Assert.assertTrue(subject.isAuthenticated());
        subject.logout();
    }

INI配置

[main]
#authenticator配置
authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator
authenticationStrategy=org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy
authenticator.authenticationStrategy=$authenticationStrategy
securityManager.authenticator=$authenticator
#authorizer配置
authorizer=org.apache.shiro.authz.ModularRealmAuthorizer
permissionResolver=org.apache.shiro.authz.permission.WildcardPermissionResolver
authorizer.permissionResolver=$permissionResolver
securityManager.authorizer=$authorizer
#realm配置
dataSource=com.alibaba.druid.pool.DruidDataSource
dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql://localhost:3306/shiro
dataSource.username=root
dataSource.password=
jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
jdbcRealm.dataSource=$dataSource
jdbcRealm.permissionsLookupEnabled=true
securityManager.realms=$jdbcRealm

java代码同前面学习的一样,获取*.ini配置文件创建SecurityManager工厂类;如果接触过IOC容器,如上配置很容易理解:
1. 对象名 = 全限定类名 相当于调用 public 无参构造器创建对象
2. 对象名. 属性名 = 值 相当于调用 setter 方法设置常量值
3. 对象名. 属性名 =$对象引用 相当于调用 setter 方法设置对象引用

INI配置分类

ini配置文件类似于Java中的properties(key=value),不过提供了将 key/value 分类的特性,key 是每个部分不重复即可,而不是整个配置文件,后边的注入会覆盖前面的注入。如下是 INI 配置分类:

[main]
#提供了对根对象securityManager及其依赖的配置
securityManager=org.apache.shiro.mgt.DefaultSecurityManager
…………
securityManager.realms=$jdbcRealm
[users]
#提供了对用户/密码及其角色的配置,用户名=密码,角色1,角色2
username=password,role1,role2
[roles]
#提供了角色及权限之间关系的配置,角色=权限1,权限2
role1=permission1,permission2
[urls]
#用于web,提供了对web url拦截相关的配置,url=拦截器[参数],拦截器
/index.html = anon
/admin/** = authc, roles[admin], perms["permission1"]

编码/加密

进制编码

实际项目中,密码的存储不是明文存储,而应该是加密存储。shiro提供了64和16进制字符串编码/解码的API支持。

    /*
     * 64进制编码解码
     */
    @Test
    public void testBase64() {
        String str = "hello";
        String base64Encoded = Base64.encodeToString(str.getBytes());
        String str1 = Base64.decodeToString(base64Encoded);
        Assert.assertEquals(str, str1);
    }
    /*
     * 16进制编码解码
     */
    @Test
    public void hex(){
        String str = "hello";
        String base16Encoded = Hex.encodeToString(str.getBytes());
        String str2 = new String(Hex.decode(base16Encoded.getBytes()));
        Assert.assertEquals(str, str2);
    }

散列算法

散列算法一般用于生成数据的摘要信息,是一种不可逆的算法,一般适合存储密码之类的数据,常见的散列算法如 MD5、SHA 等。一般进行散列时最好提供一个 salt(盐),比如加密密码 “admin”,产生的散列值是 “21232f297a57a5a743894a0e4a801fc3”,可以到一些 md5 解密网站很容易的通过散列值得到密码 “admin”,即如果直接对密码进行散列相对来说破解更容易,此时我们可以加一些只有系统知道的干扰数据,如用户名和 ID(即盐);这样散列的对象是 “密码 + 用户名 +ID”,这样生成的散列值相对来说更难破解。

CodecSupport

shiro提供了Md2、Md5、Sha1、Sha256、Sha384、Sha512散列算法,同时提供了通用的散列支持SimpleHash,其内部使用了 Java 的 MessageDigest 实现。

SimpleHash simpleHash = new SimpleHash(String algorithmName,Object source,Object salt,int hashIterations);

algorithName:加密方式,如"MD5"
source:被加密数据
slat:盐值
hashIterations:加密次数

为了方便使用,Shiro提供了HashService接口,默认提供了DefaultHashService实现。

        DefaultHashService hashService = new DefaultHashService();
        hashService.setHashAlgorithmName("MD5");
        hashService.setPrivateSalt(new SimpleByteSource("123"));
        hashService.setGeneratePublicSalt(true);
        hashService.setRandomNumberGenerator(new SecureRandomNumberGenerator());//用于生成公盐,默认就这个
        hashService.setHashIterations(1);
        HashRequest request = new HashRequest.Builder()
               .setAlgorithmName("MD5").setSource(ByteSource.Util.bytes("hello"))
                .setSalt(ByteSource.Util.bytes("123")).setIterations(2).build();
        String hex = hashService.computeHash(request).toHex();
       /* 首先创建一个 DefaultHashService,默认使用 SHA-512 算法;
        * 以通过 hashAlgorithmName 属性修改算法,默认1;
        * 可以通过 privateSalt 设置一个私盐,其在散列时自动与用户传入的公盐混合产生一个新盐;
        * 可以通过 generatePublicSalt 属性在用户没有传入公盐的情况下是否生成公盐;
        * 可以设置 randomNumberGenerator 用于生成公盐;
        * 可以设置 hashIterations 属性来修改默认加密迭代次数;
        * 需要构建一个 HashRequest,传入算法、数据、公盐、迭代次数。      
        */

PasswordService/CredentialsMatcher

Shiro 提供了 PasswordService 及 CredentialsMatcher 用于提供加密密码及验证密码服务。Shiro 默认提供了 PasswordService 实现 DefaultPasswordService;CredentialsMatcher 实现 PasswordMatcher 及 HashedCredentialsMatcher(更强大)。

DefaultPasswordService 配合 PasswordMatcher 实现简单的密码加密与验证服务

自定义Realm

public class MyRealm extends AuthorizingRealm {

    private PasswordService passwordService;

    public void setPasswordService(PasswordService passwordService) {
        this.passwordService = passwordService;
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        return new SimpleAuthenticationInfo(
                "wu",
                passwordService.encryptPassword(new String((char[])token.getCredentials())),
                getName());
    }
}

ini配置

[main]
passwordService=org.apache.shiro.authc.credential.DefaultPasswordService
hashService=org.apache.shiro.crypto.hash.DefaultHashService
passwordService.hashService=$hashService
hashFormat=org.apache.shiro.crypto.hash.format.Shiro1CryptFormat
passwordService.hashFormat=$hashFormat
hashFormatFactory=org.apache.shiro.crypto.hash.format.DefaultHashFormatFactory
passwordService.hashFormatFactory=$hashFormatFactory
passwordMatcher=org.apache.shiro.authc.credential.PasswordMatcher
passwordMatcher.passwordService=$passwordService
myRealm=com.shiro.realm.MyRealm
myRealm.passwordService=$passwordService
myRealm.credentialsMatcher=$passwordMatcher
securityManager.realms=$myRealm
  • passwordService 使用 DefaultPasswordService,如果有必要也可以自定义;
  • hashService 定义散列密码使用的 HashService,默认使用 DefaultHashService(默认 SHA-256 算法);
  • hashFormat 用于对散列出的值进行格式化,默认使用 Shiro1CryptFormat,另外提供了 Base64Format 和 HexFormat,对于有 salt 的密码请自定义实现 ParsableHashFormat 然后把 salt 格式化到散列值中;
  • hashFormatFactory 用于根据散列值得到散列的密码和 salt;因为如果使用如 SHA 算法,那么会生成一个 salt,此 salt 需要保存到散列后的值中以便之后与传入的密码比较时使用;默认使用 DefaultHashFormatFactory;
  • passwordMatcher 使用 PasswordMatcher,其是一个 CredentialsMatcher 实现;
  • 将 credentialsMatcher 赋值给 myRealm,myRealm 间接继承了 AuthenticatingRealm,其在调用 getAuthenticationInfo 方法获取到 AuthenticationInfo 信息后,会使用 credentialsMatcher 来验证凭据是否匹配,如果不匹配将抛出 IncorrectCredentialsException 异常。

HashedCredentialsMatcher 实现密码验证服务

HashedCredentialsMatcher,和之前的 PasswordMatcher 不同的是,它只用于密码验证,且可以提供自己的盐,而不是随机生成盐,且生成密码散列值的算法需要自己写,因为能提供自己的盐。

public class MyRealm2 extends AuthorizingRealm {

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        String algorithmName = "MD5";
        String username = String.valueOf(token.getPrincipal());
        String password = String.valueOf(token.getCredentials());
        String salt1 = username;
        String salt2 = new SecureRandomNumberGenerator().nextBytes().toHex();   //随机数
        int hashIterations = 2;
        SimpleHash hash = new SimpleHash(algorithmName,password,salt1+salt2,hashIterations);
        String encodedPassword = hash.toHex();
        //ByteSource.Util.bytes(salt1+salt2)为生成的新盐
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username,password,ByteSource.Util.bytes(salt1+salt2),getName());
        return info;

    }
}

ini配置

[main]
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
credentialsMatcher.hashAlgorithmName=md5
credentialsMatcher.hashIterations=2
#表示是否存储散列后的密码为 16 进制,需要和生成密码时的一样,默认是 base64;
credentialsMatcher.storedCredentialsHexEncoded=true
myRealm=com.shiro.realm.MyRealm2
myRealm.credentialsMatcher=$credentialsMatcher
securityManager.realms=$myRealm

密码重试次数限制

如在 1 个小时内密码最多重试 5 次,如果尝试次数超过 5 次就锁定 1 小时,1 小时后可再次重试,如果还是重试失败,可以锁定如 1 天,以此类推,防止密码被暴力破解。我们通过继承 HashedCredentialsMatcher,且使用 Ehcache 记录重试次数和超时时间。

public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {

    private Ehcache passwordRetryCache;

    public RetryLimitHashedCredentialsMatcher() {
        CacheManager cacheManager = CacheManager.newInstance(CacheManager.class.getClassLoader().getResource("ehcache.xml"));
        passwordRetryCache = cacheManager.getCache("passwordRetryCache");
    }

    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        String username = (String)token.getPrincipal();
        //retry count + 1
        Element element = passwordRetryCache.get(username);
        if(element == null) {
            element = new Element(username , new AtomicInteger(0));
            passwordRetryCache.put(element);
        }
        AtomicInteger retryCount = (AtomicInteger)element.getObjectValue();
        if(retryCount.incrementAndGet() > 5) {
            //if retry count > 5 throw
            throw new ExcessiveAttemptsException();
        }

        boolean matches = super.doCredentialsMatch(token, info);
        if(matches) {
            //clear retry count
            passwordRetryCache.remove(username);
        }
        return matches;
    }
}

ehcache.xml配置

<ehcache name="es">

    <diskStore path="java.io.tmpdir"/>

    <!-- 登录记录缓存 锁定1小时 -->
    <cache name="passwordRetryCache"
           maxEntriesLocalHeap="2000"
           eternal="false"
           timeToIdleSeconds="3600"
           timeToLiveSeconds="0"
           overflowToDisk="false"
           statistics="true">
    </cache>

</ehcache>

猜你喜欢

转载自blog.csdn.net/yutao_struggle/article/details/78622845
今日推荐