【shiro-安全框架】入门基础学习

安全框架常见的由两种,一种是shiro,另一种是spring security。

shiro相较于后者更加简洁,spring security可以无缝的整合spring框架

0 shiro介绍

Apache Shiro是Java 的一个安全框架。Shiro可以非常容易的开发出足够好的应用,其不仅可以用在]avaSE环境,也可以用在]avaEE环境。Shiro可以帮助我们完成:认证【登陆】、授权【权限】、加密【密码】、会话管理、与Web集成、缓存等。

1 shiro中常见的概念

在这里插入图片描述

Subject:

Subject主体,外部应用与Subject进行交互,Subject将用户作为当前操作的主体,这个主体:可以是一个通过浏览器请求的用户,也可能是一个运行的程序。Subject在shiro中是一个接口,接口中定义了很多认证授相关的方法,外部程序通过Subject进行认证授,而Subject是通过SecurityManager安全管理器进行认证授权。

SecurityManager:

SecurityManager权限管理器,它是shiro的核心,负责对所有的subject进行安全管理。通过SecurityManager可以完成subject的认证、授权等。SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等。SecurityManager是一个接口,继承了Authenticator,Authorizer,SessionManager这三个接口。

Authenticator:

Authenticator即认证器,对用户登录时进行身份认证。

Authorizer:

Authorizer授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。

Realm(数据库读取+认证功能+授权功能实现):

Realm领域,相当于datasource数据源,SecurityManager进行安全认证需要通过Realm获取用户权限数据。

比如:如果用户身份数据在数据库那么realm就需要从数据库获取用户身份信息。

注意:不要把realm理解成只是从数据源取数据,在realm中还有认证授权校验的相关的代码。

SessionManager:

SessionManager会话管理,shiro框架定义了一套会话管理,它不依赖web容器的session,所以shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录。

SessionDAO:

SessionDAO即会话dao,是对session会话操作的一套接口。

比如:可以通过jdbc将会话存储到数据库也可以把session存储到缓存服务器。

CacheManager:

cacheManager缓存管理,将用户权限数据存储在缓存,这样可以提高性能。

Cryptography:

Cryptography密码管理,shiro提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能。

2 快速入门shiro

2.1 认证的流程

在这里插入图片描述

流程如下:

首先调用 Subject.login(token)进行登录,其会自动委托给 Security Manager,调用之前必须通过 Securityutils.setSecurityManager()设置;

SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;

Authenticator 才是真正的身份验证者,Shiro API 中核心的身份认证入口点,此处可以自定义插入自己的实现;

Authenticator可能会委托给相应的AuthenticationStrategy 进行多Realm身份验证,默认ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm 身份验证;

Authenticator会把相应的 token传入 Realm,从 Realm获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处可以配置多个Realm,将按照相应的顺序及策略进行访问。

(1)在maven项目中引入相关依赖

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>${shiro-version}</version>
</dependency>

(2)创建.ini文件模拟数据

[users]===表示user表
admin=123456
zs=123456

(3)编写代码测试

package test;

public class test01 {
    public static void main(String[] args) {
        
        //创建SecurityManager对象。
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        
        //为SecurityManager指定Realm对象,这里要使用classpath指定编译后的资源文件
    	IniRealm realm=new IniRealm("classpath:shiro.ini");
        
        //通过SecurityUtil把SecurityManager设置上下文可用。生效问题。
        SecurityUtils.setSecurityManager(securityManager);
        
        //获取Subject对象
        Subject subject = SecurityUtils.getSubject();
        
        //完成认证功能AuthenticationToken:该类把账号和密码封装到该类中
        UsernamePasswordToken token=new UsernamePasswordToken("admin","123456");
        try {
            subject.login(token);
            System.out.println("登录成功");
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("账号或密码错误");
        }
       
        System.out.println("是否认证成功:"+subject.isAuthenticated());
        //退出登录
        subject.logout();
        System.out.println("是否认证成功:"+subject.isAuthenticated());                       
    }
}


2.2 授权

一定是在认证成功后才会执行的代码。

ini文件:

[users]
#账号=密码,角色
admin=123456,admin
ldh=123456

[roles]---权限
#角色=权限
admin=user:query,user:delete,user:save,user:update
role1=user:query,user:export
package test;

public class test02 {
    public static void main(String[] args) {
        
        //创建SecurityManager对象。
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        
        //为SecurityManager指定Realm对象,这里要使用classpath指定编译后的资源文件
    	IniRealm realm=new IniRealm("classpath:shiro.ini");
        
        //通过SecurityUtil把SecurityManager设置上下文可用。生效问题。
        SecurityUtils.setSecurityManager(securityManager);
        
        //获取Subject对象
        Subject subject = SecurityUtils.getSubject();
        
        //完成认证功能AuthenticationToken:该类把账号和密码封装到该类中
        UsernamePasswordToken token=new UsernamePasswordToken("admin","123456");
        try {
            subject.login(token);
            System.out.println("登录成功");
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("账号或密码错误");
        }
        
        //判断当前用户是否具有某个权限
        boolean permitted = subject.isPermitted("user:query");
        System.out.println("判断当前用户是否具有User:query权限:" + permitted);
           
        //判断当前用户是否同时具有权限,与关系
        boolean permittedAll = subject.isPermittedAll("user:query", "user:save");
        System.out.println("判断当前用户是否同时具有user:query和user:save权限:"+permittedAll);
                               
        //只要有一个权限就输出一个true,或关系
        boolean[] permitted1 = subject.isPermitted("user:query", "user:export");
        System.out.println(Arrays.toString(permitted1));
    }
}
                            

上面写的代码中所有的数据都是基于ini文件中得来,实际开发中,数据的来源应该是从数据库中拿到的。这时我们可以自定义realm,可以帮我们完成相应的认证功能。

3.自定义realm请求

3.1 认证方法

(1)User实体类

package entity;

@Data
public class User {
    private Integer id;
    private String username;
    private String password;
    private String realname;
    
}

(2)service代码

package service;

public class UserService {
    public User findByUsername(String username) {
    	if("admin".equals(username)) {
            return new User(1,username,"123456","管理员");
        } else if ("YKQ".equals(username)) {
            return new User(2, username, "123456", "ykq");
        } else {
            return null;
        }
    }
} 

(3)创建一个类并继承AuthorizingRealm

package realm;

public class MyRealm extends AuthorizingRealm {
    
    private UserService userService = new UserService();
    
    //授权时执行该方法
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("授权方法===");
        return null;
    }
    
    //认证时执行该方法
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException{
        System.out.println("认证方法===");
        
        //获取登录者的账号
        String username = authenticationToken.getPrincipal.toString();
        
        //根据账号查询数据库信息
        User user = userService.findByUsername(username);
        if(user != null) {
            //Object principal,数据库中的账号
            //Object credentials,密码
            //String realmName,realmName名称,随便起
            //密码的比对交给了 SimpleAuthenticationInfo 这个类
            SimpleAuthenticationInfo info=new SimpleAuthenticationInfo(username, user.getPassword, this.getName);
            return info;
        }
        
    	return null;
    }
}

(4)修改securityManager的realm对象

package test;

public class test03 {
    public static void main(String[] args) {
        
        //创建SecurityManager对象。
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        
        //为SecurityManager指定Realm对象,这里要使用classpath指定编译后的资源文件
    	MyRealm realm=new MyRealm();
        securityManager.setRealm(realm);
        //通过SecurityUtil把SecurityManager设置上下文可用。生效问题。
        SecurityUtils.setSecurityManager(securityManager);
        
        //获取Subject对象
        Subject subject = SecurityUtils.getSubject();
        
        //完成认证功能AuthenticationToken:该类把账号和密码封装到该类中
        UsernamePasswordToken token=new UsernamePasswordToken("admin","123456");
        try {
            subject.login(token);
            System.out.println("登录成功");
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("账号或密码错误");
        }
        
        //判断当前用户是否具有某个权限
        boolean permitted = subject.isPermitted("user:query");
        System.out.println("判断当前用户是否具有User:query权限:" + permitted);
           
        //判断当前用户是否同时具有权限,与关系
        boolean permittedAll = subject.isPermittedAll("user:query", "user:save");
        System.out.println("判断当前用户是否同时具有user:query和user:save权限:"+permittedAll);
                               
        //只要有一个权限就输出一个true,或关系
        boolean[] permitted1 = subject.isPermitted("user:query", "user:export");
        System.out.println(Arrays.toString(permitted1));
    }
}
              

3.2 完成自定义授权

package realm;

public class MyRealm extends AuthorizingRealm {
    
    private UserService userService = new UserService();
    
    //只有登录者登录成功时才会执行,授权时执行该方法
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("授权方法===");
        //获取当前登录者的账号
        String username = (String)principalCollection.getPrimaryPrincipal();
        
        //根据账号查询当前账号具有的权限
        List<String> permissions = userService.findPermissionByUsername(username);
        
        if(permissions.size() > 0){
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
            info.addStringPermissions(permissions);
            return info;
        }
        return null;
    }
    
    //认证时执行该方法
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException{
        System.out.println("认证方法===");
        
        //获取登录者的账号
        String username = authenticationToken.getPrincipal.toString();
        
        //根据账号查询数据库信息
        User user = userService.findByUsername(username);
        if(user != null) {
            //Object principal,数据库中的账号
            //Object credentials,密码
            //String realmName,realmName名称,随便起
            //密码的比对交给了 SimpleAuthenticationInfo 这个类
            SimpleAuthenticationInfo info=new SimpleAuthenticationInfo(username, user.getPassword, this.getName);
            return info;
        }
        
    	return null;
    }
}
package service;

public class UserService {
    public User findByUsername(String username) {
    	if("admin".equals(username)) {
            return new User(1,username,"123456","管理员");
        } else if ("YKQ".equals(username)) {
            return new User(2, username, "123456", "ykq");
        } else {
            return null;
        }
    }
    
    public List<String> findPermissionByUsername(String username) {
        List<String> list=new ArrayList<String>();
        if("admin".equals(username)){
            list.add("user:query");
            list.add("user:save");
            list.add("user:delete");
            list.add("user:update");l
        } else if ("YKQ".equals(username)){
            list.add("user:query");
            list.add("user:export");
        }
        return list;
    }
} 

4 密码加密

4.1 加密演示

在这里插入图片描述

shiro默认采用md5加密,我们可以写一个单元测试来体现这一点。

public class Test04 {
    public static void main(String[] args) {
        //Md5Hash中的参数表示要加密的明文
        Md5Hash md5Hash = new Md5Hash("123456");
        //输出加密后的密文
        System.out.println(md5Hash);
    }
}

解密网址会把常见的加密的内容搜集起来---造成了数据不安全。我们可以使用加盐加密来避免这一点。

public class Test04 {
    public static void main(String[] args) {
        
        //加盐加密,第二个参数就是 盐值 ,这个盐不要泄露
        Md5Hash md5Hash1=new Md5Hash("123456", "aaa");
        
        //输出加密后的密文 
        System.out.println(md5Hash);
    }
}

我们还可以使用加盐加密,同时设置加密次数

public class Test04 {
    public static void main(String[] args) {
        
        //加盐加密,同时设置加密次数, 第三个参数就是 加密次数
        Md5Hash md5Hash1=new Md5Hash("123456", "aaa", 1024);
        
        //输出加密后的密文 
        System.out.println(md5Hash1);
    }
}

4.2 shiro中使用md5加密

(1)设置realm加密器

package test;

public class test05 {
    public static void main(String[] args) {
        
        //创建SecurityManager对象。
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        
        //为SecurityManager指定Realm对象,这里要使用classpath指定编译后的资源文件
    	MyRealm realm=new MyRealm();
        
        //创建Hash加密器
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        matcher.setHashAlgorithmName("MD5");//设置加密方式
        MD5matcher.setHashIterations(1024);//散列的次数1024
        myRealm.setcredentialsMatcher(matcher);//设置realm的密码匹配器
        
        securityManager.setRealm(realm);
        //通过SecurityUtil把SecurityManager设置上下文可用。生效问题。
        SecurityUtils.setSecurityManager(securityManager);
        
        //获取Subject对象
        Subject subject = SecurityUtils.getSubject();
        
        //完成认证功能AuthenticationToken:该类把账号和密码封装到该类中
        UsernamePasswordToken token=new UsernamePasswordToken("admin","123456");
        try {
            subject.login(token);
            System.out.println("登录成功");
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("账号或密码错误");
        }
        
        //判断当前用户是否具有某个权限
        boolean permitted = subject.isPermitted("user:query");
        System.out.println("判断当前用户是否具有User:query权限:" + permitted);
           
        //判断当前用户是否同时具有权限,与关系
        boolean permittedAll = subject.isPermittedAll("user:query", "user:save");
        System.out.println("判断当前用户是否同时具有user:query和user:save权限:"+permittedAll);
                               
        //只要有一个权限就输出一个true,或关系
        boolean[] permitted1 = subject.isPermitted("user:query", "user:export");
        System.out.println(Arrays.toString(permitted1));
    }
}
            

在上面的创建Hash加密器中,我们并没有使用加盐值的方式。原因是在这里设置不合适,如果在这里设置盐值的话,就不能再改变盐值了。应该每个账号都应该有自己的盐值。

我们可以再realm中设置盐值。

(2)修改自定义的realm参数(多了盐值)

package realm;

public class MyRealm extends AuthorizingRealm {
    
    private UserService userService = new UserService();
    
    //只有登录者登录成功时才会执行,授权时执行该方法
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("授权方法===");
        //获取当前登录者的账号
        String username = (String)principalCollection.getPrimaryPrincipal();
        
        //根据账号查询当前账号具有的权限
        List<String> permissions = userService.findPermissionByUsername(username);
        
        if(permissions.size() > 0){
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
            info.addStringPermissions(permissions);
            return info;
        }
        return null;
    }
    
    //认证时执行该方法
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException{
        System.out.println("认证方法===");
        
        //获取登录者的账号
        String username = authenticationToken.getPrincipal.toString();
        
        //根据账号查询数据库信息
        User user = userService.findByUsername(username);
        if(user != null) {
            //Object principal,数据库中的账号
            //Object credentials,密码
            //ByteSource credentialsSalt:使用的盐
            //String realmName,realmName名称,随便起
            //密码的比对交给了 SimpleAuthenticationInfo 这个类
            //由于ByteSource是个接口 我们不可以直接new出来,我们要么new它的子类,要么通过工厂来拿到它
            ByteSource salt=ByteSource.Util.bytes( string: "aaa");//盐值未来应该存在数据库中,从数据库拿到user的时候一并把盐值拿到 user.getSalt
            
            //shiro会帮我们进行密码的比对。shiro会先对你的密码进行加密,再和数据库中的加密后的密码进行比较
            SimpleAuthenticationInfo info=new SimpleAuthenticationInfo(username, user.getPassword, salt, this.getName);
            return info;
        }
        
    	return null;
    }
}

(3)修改数据库的明文密码,改为加密后的密码

end
  • 作者:dicraft(联系作者)
  • 更新时间:2022-10-11 09:37
  • 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
  • 转载声明:如果是转载栈主转载的文章,请附上原文链接
  • 评论

    新增邮件回复功能,回复将会通过邮件形式提醒,请填写有效的邮件!