shiro入门学习四

来源:互联网 发布:永宏触摸屏编程软件 编辑:程序博客网 时间:2024/05/27 14:14

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.ModularRealmAuthenticatorauthenticationStrategy=org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategyauthenticator.authenticationStrategy=$authenticationStrategysecurityManager.authenticator=$authenticator#authorizer配置authorizer=org.apache.shiro.authz.ModularRealmAuthorizerpermissionResolver=org.apache.shiro.authz.permission.WildcardPermissionResolverauthorizer.permissionResolver=$permissionResolversecurityManager.authorizer=$authorizer#realm配置dataSource=com.alibaba.druid.pool.DruidDataSourcedataSource.driverClassName=com.mysql.jdbc.DriverdataSource.url=jdbc:mysql://localhost:3306/shirodataSource.username=rootdataSource.password=jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealmjdbcRealm.dataSource=$dataSourcejdbcRealm.permissionsLookupEnabled=truesecurityManager.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,角色2username=password,role1,role2[roles]#提供了角色及权限之间关系的配置,角色=权限1,权限2role1=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.DefaultPasswordServicehashService=org.apache.shiro.crypto.hash.DefaultHashServicepasswordService.hashService=$hashServicehashFormat=org.apache.shiro.crypto.hash.format.Shiro1CryptFormatpasswordService.hashFormat=$hashFormathashFormatFactory=org.apache.shiro.crypto.hash.format.DefaultHashFormatFactorypasswordService.hashFormatFactory=$hashFormatFactorypasswordMatcher=org.apache.shiro.authc.credential.PasswordMatcherpasswordMatcher.passwordService=$passwordServicemyRealm=com.shiro.realm.MyRealmmyRealm.passwordService=$passwordServicemyRealm.credentialsMatcher=$passwordMatchersecurityManager.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.HashedCredentialsMatchercredentialsMatcher.hashAlgorithmName=md5credentialsMatcher.hashIterations=2#表示是否存储散列后的密码为 16 进制,需要和生成密码时的一样,默认是 base64;credentialsMatcher.storedCredentialsHexEncoded=truemyRealm=com.shiro.realm.MyRealm2myRealm.credentialsMatcher=$credentialsMatchersecurityManager.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>