shiro一款简易的Java安全框架

这两天因为项目中一直有用到shiro这款框架,所以也是趁着休息的时间好好补课一下shiro。

一、什么是shiro?

shiro是一个强大的Java安全框架,执行身份验证。授权、密码和会话管理的。使用shiro易于理解API,可以非常方便的集成到任何应用程序中。

在这里也说明一下:关于Spring Security这款安全框架,两者在功能上是非常类似的,所以最好在学习完一种后最后把另一种也学习一下。

二、shiro的三大核心组件

1、subject:简单理解为表示当前操作用户。其实它的深层表达的意思是第三方的进程。意味这与当前系统交互的"东西"。
那这里人是直接操作我们的系统,所以一般情况下,subject就是当前用户,可以通过subject轻松获取需要登录认证的用户名密码涉及到安全的操作数据。

2、securityManager : 它是shiro框架的核心,是一种典型的Facade模式,shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。

3、Realm: Realm是shiro框架中与应用安全数据间的桥梁或者连接器,直白的说当用户需要登录认证/授权的时候,shiro会从应用配置的Realm中查找用户及其权限信息。

其中值得注意的是:在配置shiro时,你必须至少指定一个Realm,用户认证或者授权,可以配置多个Realm.

三、SpringBoot集成Shiro

<dependency>
   <groupId>org.apache.shiro</groupId>
   <artifactId>shiro-spring</artifactId>
   <version>1.3.2</version>
</dependency>

在项目中我们一般会配置一个shiroConfig配置类,这里面会配置我们shiro中非常重要的认证、访问控制信息以及我们上面说的Realm的管理。

我把我写的一个shiroConfig例子写出来大家参考一下:

/**
 * 2020/05/20
 */
@Configuration
public class ShiroConfig {
    //配置Shiro的安全管理器
    @Bean
    public SecurityManager securityManager(Realm myRealm){
        DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
        //设置一个Realm,这个Realm是最终用于完成我们的认证号和授权操作的具体对象
        securityManager.setRealm(myRealm);
        return securityManager;
    }
    //配置一个自定义的Realm的bean,最终将使用这个bean返回的对象来完成我们的认证和授权
    @Bean
    public Realm myRealm(){
        MyRealm realm=new MyRealm();
        return realm;
    }

    //配置一个Shiro的过滤器bean,这个bean将配置Shiro相关的一个规则的拦截
    //例如什么样的请求可以访问什么样的请求不可以访问等等
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
        //创建Shiro的拦截的拦截器 ,用于拦截我们的用户请求
        ShiroFilterFactoryBean shiroFilter=new ShiroFilterFactoryBean();
        //设置Shiro的安全管理,设置管理的同时也会指定某个Realm 用来完成我们权限分配
        shiroFilter.setSecurityManager(securityManager);
        //用于设置一个登录的请求地址,这个地址可以是一个html或jsp的访问路径,也可以是一个控制器的路径
        //作用是用于通知Shiro我们可以使用这里路径转向到登录页面,但Shiro判断到我们当前的用户没有登录时就会自动转换到这个路径
        //要求用户完成成功
        shiroFilter.setLoginUrl("/");
                //登录成功后转向页面,由于用户的登录后期需要交给Shiro完成,因此就需要通知Shiro登录成功之后返回到那个位置
        shiroFilter.setSuccessUrl("/success");
        //用于指定没有权限的页面,当用户访问某个功能是如果Shiro判断这个用户没有对应的操作权限,那么Shiro就会将请求
        //转向到这个位置,用于提示用户没有操作权限
        shiroFilter.setUnauthorizedUrl("/noPermission");
        //定义一个Map集合,这个Map集合中存放的数据全部都是规则,用于设置通知Shiro什么样的请求可以访问什么样的请求不可以访问
        Map<String,String> map=new LinkedHashMap<String,String>();
        //  /login 表示某个请求的名字    anon 表示可以使用游客什么进行登录(这个请求不需要登录)
        map.put("/login","anon");
       //我们可以在这里配置所有的权限规则这列数据真正是需要从数据库中读取出来
        //或者在控制器中添加Shiro的注解
        //  /admin/**  表示一个请求名字的通配, 以admin开头的任意子孙路径下的所有请求
        //  authc 表示这个请求需要进行认证(登录),只有认证(登录)通过才能访问
        // 注意: ** 表示任意子孙路径
        //       *  表示任意的一个路径
        //       ? 表示 任意的一个字符
        map.put("/admin/**","authc");
        map.put("/user/**","authc");
        //表示所有的请求路径全部都需要被拦截登录,这个必须必须写在Map集合的最后面,这个选项是可选的
        //如果没有指定/** 那么如果某个请求不符合上面的拦截规则Shiro将方行这个请求
//        map.put("/**","authc");
        shiroFilter.setFilterChainDefinitionMap(map);
        return shiroFilter;
    }
}

里面配置了一些简单的登录URL、登录成功的URL、和nopermission没有权限的URL。然后又MyRealm对象交由spring来管理。后面的有一个类似于拦截器功能的/admin/** … 。作为需要认证还是不需要认证、需要相应角色、权限等。

MyRealm:

/**
 * 2020/05/20
 */
public class MyRealm extends AuthorizingRealm  {

    /**
     *用户认证的方法 这个方法不能手动调用 , Shiro会自动调用
     * @param authenticationToken 用户身份 这里存放的是用户的账号和密码
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        String username=token.getUsername();//获取页面中传递的用户账号
        String password=new String(token.getPassword());//获取页面中的用户密码实际工作中基本不需要获取
        System.out.println(username+" -----  "+password);

        /**
         * 认证账号,这里应该从数据库中获取数据,
         * 如果进入if表示账号不存在要抛出异常
         */
        if(!"admin".equals(username)&&!"zhangsan".equals(username)&&!"user".equals(username)){
            throw new UnknownAccountException();//抛出账号错误的异常
        }

        /**
         * 认证账号,这里应该根据从数据库中获取数来的数据进行逻辑判断,判断当前账号是否可用
         * IP是否允许等等,根据不同的逻可以抛出不同的异常
         */
        if("zhangsan".equals(username)){
            throw new LockedAccountException();//抛出账号锁定异常
        }
        /**
         * 数据加密主要是防止数据在浏览器到后台服务器之间的数据传递时被篡改或被截获,因此应该在前台到后台的过程中进行加密
         * 注意:
         *   建议浏览器传递数据时就是加密数据,数据库中存在的数据也是加密数据,我们必须保证前段传递的数据
         *   和数据主库中存放的数据加密次数以及盐一会规则都是完全相同的否则认证失败
         */
        //设置让当前登录用户中的密码数据进行加密
//        HashedCredentialsMatcher credentialsMatcher=new HashedCredentialsMatcher();
//        credentialsMatcher.setHashAlgorithmName("MD5");
//        credentialsMatcher.setHashIterations(2);
//
//        this.setCredentialsMatcher(credentialsMatcher);

        //对数据库中的密码进行加密
//        Object obj = new SimpleHash("MD5","123456","",1);
        return new SimpleAuthenticationInfo(username,"e10adc3949ba59abbe56e057f20f883e",getName());
    }

    /**
     * 用户授权的方法,当用户认证通过每次访问需要访问需要授权时都会执行这段代码完成授权操作
     * 这里用查询数据库来获取当前用户的所有角色和权限,并设置到shiro中
     * 注意:由于每次点击需要授权的请求时,Shiro都会执行这个方法,因此如果这里的数据时来自于数据库中的
     *      那么一定要控制好不能每次都从数据库中获取数据这样效率太低了
     * @param principalCollection
     * @return
     */

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {

        //获取用户的账号,根据账号来从数据库中获取数据
        Object obj = principalCollection.getPrimaryPrincipal();
        //定义用户角色的set集合这个集合应该来自数据库
        Set<String> roles = new HashSet<>();
        if("admin".equals(obj)){
            System.out.println(" ---  授权了admin --------");
            roles.add("admin");
            roles.add("user");
        }
        if("user".equals(obj)){
            System.out.println(" ---  授权了user --------" + obj);
            roles.add("user");
        }
        Set<String> permissions = new HashSet<>();
        if("admin".equals(obj)){
            //添加一个权限admin:add 只是一种命名风格表示admin下的add功能
            permissions.add("admin:add");
        }
        SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();

        info.setRoles(roles);
        info.setStringPermissions(permissions);

        return info;
    }

那一个最基本的Realm类就创建好了,可以看到里面的两个方法:
1、doGetAuthenticationInfo shiro在 用户认证的时候自动调用这个方法。可以看到这个方法的参数其实就用一个简易的用户身份令牌。这个用户自然就是需要认证的用户,从令牌中我们可以取出当前认证用户的用户名和登录密码。自然后面有密码加密如MD5等等。经过多少次的加盐迭代来最终判断该用户的身份。

2、doGetAuthorizationInfo 授权的回调方法。可以看到这个方法里面就是给当前用户授权的操作。

那边方便大家的测试给大家写了controller:

/**
 * 2020/05/20
 */

@Controller
public class TestController {

    @RequestMapping("/")
    public String index(){
        return "login";
    }

    @RequestMapping("/login")
    public  String login(String username, String password, Model model){
        //获取权限操作对象,利用这个对象来完成登录操作
        Subject subject= SecurityUtils.getSubject();
        //登出,进入这个请求用户一定是要完成用户登录功能,因此我们就先登出,否则Shiro会有缓存不能重新登录
        //注意:这么做如果用户是误操作会重新指定一次登录请求
        subject.logout();
        //用户是否认证过(是否登录过),进入if表示用户没有认证过需要进行认证
        if(!subject.isAuthenticated()){
            //创建用户认证时的身份令牌,并设置我们从页面中传递过来的账号和密码
            UsernamePasswordToken usernamePasswordToken=new UsernamePasswordToken(username,password);
            try {

                /**
                 * 指定登录,会自动调用我们Realm对象中的认证方法
                 * 如果登录失败会抛出各种异常
                 */
                subject.login(usernamePasswordToken);
            } catch (UnknownAccountException e) {
//                e.printStackTrace();
                model.addAttribute("errorMessage","账号错误!");
                return "login";
            }catch (LockedAccountException e) {
//                e.printStackTrace();
                model.addAttribute("errorMessage","账号被锁定!");
                return "login";
            }catch (IncorrectCredentialsException e) {
//                e.printStackTrace();
                model.addAttribute("errorMessage","密码错误");
                return "login";
            }catch (AuthenticationException e) {
                e.printStackTrace();
                model.addAttribute("errorMessage","认证失败!");
                return "login";
            }
        }

        return "redirect:/success";
    }

    @RequestMapping("/logout")
    public String logout(){
        Subject subject = SecurityUtils.getSubject();
        //清空当前账号shiro的缓存,否则无法重新登录
        subject.logout();
        return "redirect:/";
    }

    @RequestMapping("/success")
    public String loginSuccess(){
        return "success";
    }
    @RequestMapping("/noPermission")
    public String noPermission(){
        return "noPermission";
    }


    @RequiresRoles(value = {"admin"})
    @RequestMapping("/admin/test")
    public @ResponseBody
    String adminTest(){
        return "/admin/test请求";
    }

    /**
     * 必须登录认证才可以访问,不需要用户拥有角色和权限
     * @return
     */
    @RequiresAuthentication
    @RequestMapping("/admin/test01")
    public @ResponseBody String adminTest01(){
        return "/admin/test01请求";
    }
    /**
     * @RequiresPermissions 用于判断当前用户是否有指定的一个或多个权限用法与RequiresRoles相同
     */
    @RequiresPermissions(value={"admin:add"})
    @RequestMapping("/admin/add")
    public @ResponseBody String adminAdd(){
        return "/admin/add请求";
    }

    @RequiresRoles(value = {"user"})
    @RequestMapping("/user/test")
    public @ResponseBody String userTest(){
        return "/user/test请求";
    }

    /**
     * 配置自定义异常的拦截需要拦截authorizationException或者shiroException
     */
    @ExceptionHandler(value={AuthorizationException.class})
    public String permissionError(Throwable throwable){
        //转向到没有权限的视图页面,可以利用参数throwable将错误信息写入浏览器中
        //实际工作工作中应该根据参数的类型来判断具体是什么异常,然后根据同的异常来为用户提供不同的
        //提示信息
        return "noPermission";
    }
}

login.html登录页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script th:src="@{|/js/jquery-1.11.3.min.js|}"></script>
    <script th:src="@{|/js/jQuery.md5.js|}"></script>
    <script>
        $(function(){
            $("#loginBut").bind("click",function(){
                var v_md5password=$.md5($("#password").val());
                $("#md5Password").val(v_md5password)
            })
        })
    </script>
</head>
<body>
<form action="login" method="post">
    账号<input type="text" name="username"><br>
    密码<input type="text"  id="password"><br>
    <input type="hidden" name="password" id="md5Password">
    <input type="submit" value="登录" id="loginBut">
</form>

<span style="color: red" th:text="${errorMessage}"></span>

</body>
</html>

noPermission.html 没有权限页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>对不起!您没有权限操作!</h1>
</body>
</html>

success.html 登录成功页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!--基于属性的shiro标签-->
<h1 shiro:guest="true">  没有登录</h1>
<!--基于标签的-->
<shiro:authenticated>
    <h1>登录成功</h1><br>
</shiro:authenticated>

<a href="/logout">登出</a><br><br><br>

<a th:href="@{|/admin/test|}">需要有admin角色的功能</a><br>
<a th:href="@{|/admin/test01|}">需要有admin角色的功能test01</a><br>
<a th:href="@{|/admin/add|}">需要有admin角色的功能add</a><br>
<a th:href="@{|/user/test|}">需要有user角色的功能</a><br>
</body>
</html>

这样一个最简单的shiro项目就搭建成功了,但是我想说的是开发中shiro框架不会这样使用。特别是在某些URL需要特定的角色和权限的时候,这样在配置文件中大量配置,增加了项目的维护成本。后期URL地址特别多可能我们的shiroConfig这个配置类会非常庞大。因此下面一张我想说的是shiro的基于注解的开发以及常见的shiro标签(shiro集成thymeleaf的用法)。

–未完待续

ok,周末愉快了~ ~ ~

猜你喜欢

转载自blog.csdn.net/qq_42963930/article/details/106319813