权限验证框架之Shiro

前言

交替换个脑子,一直搞考研的东西,实在是无聊。所以顺便把工程上的东西,拿来遛一遛。你问我,为啥不是机器学习,深度学习,那玩意搞起来头更大,累了。权当是打游戏放松了,那么废话不多说,这里要玩玩的是Shiro,其实一开始我还是喜欢玩这个Security,不过后来,经常用这个人人开源,也就接触这个玩意了,说实话,先前用那个玩意的时候,也是习惯性的把shiro改成security,但是实话实说,太麻烦了,懒得改,所以的话,干脆就是直接使用这个Shiro。

当然关于权限验证,其实我们自己基于RBAC权限管理模型直接做一套都是可以的,基于Spring的AOP,快速做一个简单的这个是非常快的。包括,当初我写的那个WhitHoleV0.7版本其实那个用户端的权限验证都是自己做的。ok,说多了,我们来快速开始吧。

当然自己动手实现一个权限验证其实也不难,shiro只是提供了一个架子而已。后面有时间的话,我们可以直接自己写一个Shiro lite 或者security lite拿过来玩玩。

shiro 核心

ok,我们开始,首先的话,这个shiro由如下模块组成:
在这里插入图片描述

Authentication:身份认证/登录,验证用户是不是拥有相应的身份

Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能进行什么操作,如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限

Session
Management:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境,也可以是Web
环境的

Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储

Web Support:Web 支持,可以非常容易的集成到Web 环境

Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率

Concurrency:Shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去

Testing:提供测试支持

“Run As”:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问

Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了

从我们的使用角度来看,它的运行流程大致如下:
在这里插入图片描述
也是分为几个部分:

  1. subject:这个是对User信息的一些封装
  2. SecurityManager: 里面实现了对用户信息授权,认证的一些操作
  3. Realm: 和数据库打交道,比如验证用户权限,这个我们需要查表,那么这个时候,我们就需要这个玩意

也就是说,subject过来之后,通过Manager,去执行对于的执行权限的方法,在进行用户验证的时候,将使用到Realm,去读取数据,之后完成操作。

项目构建

默认Session模式

现在虽然比较流行的是这个前后端分离架构,用的是token,但是,很久以前,还没有分离的时候,还是用的这个,

那么在这边进行整合的时候是这样的:
在这里插入图片描述
然后我们导入一下,配置,我这里的话,还导入了这个web starter。这里做演示,我就不建表了。

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--引入shrio-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-starter</artifactId>
        </dependency>


        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

配置

那么我们先来看到配置,看看我们看到了流程图,我们其实发现,就是说,我们的请求其实是首先到了一个过滤器,然后在这个过滤器里面进行操作,拦截的,完成权限的认证的。然后,刚刚也说到,完成认证是这样的:

  1. 拦截到请求
  2. 进入到安全管理器
  3. 管理器负责调度对应的认证,其中我们要使用到Realm,去完成这个从数据库,或者说是认证的具体实现。
  4. 然后就是责任链一路放行,比如验证成功,一路放行到资源,如果失败,就怎么怎么样,这里面有一套操作,我们通过传入到下一层的状态,来判断当前的处理器,要不要处理,然后一条链路走下来,直到走完,或者提前结束。

那么其实都说到这里了,没有接触过Shiro但是,项目写多了的朋友,都看到这个份上了,估计手写一个dome都可以了(真的!)当然,里面还是有很多的一些细节不一样是吧,但是大体大致的一定可以写出来了。

所以,首先,我们要用,就需要先写个Realm.

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

/**
 * 自定义Realm
 */
public class CustomerRealm extends AuthorizingRealm {
    
    
    //实现授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    
    
        return null;
    }
    //实现认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    
    
        return null;
    }
}

然后呢,我们还要写过Config,给到容器,这里的话,我们使用了这个shiro-starter。所以写完Config之后的话,我们可以就是说可以和SpringBoot一起启动,或者一起注入到Servlet里面,完成运行。

package com.huterox.shirodome.config;

import com.huterox.shirodome.Shiro.CustomerRealm;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {
    
    
    //ShiroFilter过滤所有请求
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
    
    
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //给ShiroFilter配置安全管理器
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //配置那些需要放行,需要拦截,需要怎么怎么样
        /*这里有对应的注解
        *anon:无需认证就可以访问
        *authc:必须认证了才能让问
        *user:必须拥有记住我功能才能用
        *perms:拥有对某个资源的权限才能访问[角色:操作];
        *roLe:拥有某个角色权限才能访问[角色]
    
        * @RequiresAuthentication:必须经过认证才能访问
          @RequiresUser:必须经过认证,并且有记住我功能才能访问
          @RequiresPermissions("permission:operation"):需要拥有指定权限才能访问,其中permission为资源名,operation为操作名
          @RequiresRoles("roleName"):需要拥有指定角色(roleName)才能访问
        * */
        Map<String, String> map = new HashMap<String, String>();
        map.put("/hello","anon");
        map.put("/admin","authc");
//        map.put("/superAdmin","perms[s:p]");
        //去登陆接口,没有通过验证进入
        shiroFilterFactoryBean.setLoginUrl("toLogin");
        shiroFilterFactoryBean.setUnauthorizedUrl("/noauthor");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }
    //创建安全管理器
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm) {
    
    
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm);
        return securityManager;
    }
    //创建自定义Realm
    @Bean
    public Realm getRealm() {
    
    
        CustomerRealm realm = new CustomerRealm();
        return realm;
    }
    // 对Shiro注解的支持
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
    
    
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        creator.setProxyTargetClass(true);
        return creator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
    
    
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;

    }
}


测试接口

ok,那么看完了这个,我们来看到,我们这边准备了那些接口。

package com.huterox.shirodome.controller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
public class ShiroHelloController {
    
    

    @RequestMapping("/hello")
    public String hello(){
    
    
        return "Hello";
    }

    @RequestMapping("/admin")
    public String admin(){
    
    
        return "Admin";
    }

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

    @RequestMapping("/noauthor")
    public String noauthor(){
    
    
        return "木有权限";
    }


    @RequestMapping("/superAdmin")
    @RequiresPermissions("s:p")
    public String SP(){
    
    
        return "高贵的SP你好";
    }



    @RequestMapping("/login")
    public String login(String username,String password){
    
    
        //获取到用户对象,并且封装起来,方便后面shiro使用
        Subject subject = SecurityUtils.getSubject();
//        如果这里还要采用md5”加密“的话
//        String salt= "Huterox";
//        String passwordSalt = new SimpleHash("MD5", password, salt, 2).toString();
//        UsernamePasswordToken token = new UsernamePasswordToken(username,passwordSalt);
        UsernamePasswordToken token = new UsernamePasswordToken(username,password);
        try {
    
    
            subject.login(token);
            System.out.println("登录成功!!!");
            return "OK";
        } catch (UnknownAccountException e) {
    
    
            System.out.println("用户错误!!!");
        } catch (IncorrectCredentialsException e) {
    
    
            System.out.println("密码错误!!!");
        }
        return "NO";
    }
}

因为我们这边是在做认证和授权,所以的话,我们这边有,公共接口,登录接口,拥有特殊授权才能访问的接口。当然还有未授权返回的接口。

Realm编写

在我们的这个Shiro当中,最重要的其实就是这个玩意的实现,在这里,我们要完成的是用户的Authentication和Authorization。

在这里的话,我们这边有两个角色,一个是admin,还有一个是superAdmin。密码都是admin。其他的话,在代码里面有很详细的注释:

package com.huterox.shirodome.Shiro;

import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

/**
 * 自定义Realm
 */
public class CustomerRealm extends AuthorizingRealm {
    
    
    //实现授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    
    
        System.out.println("授权当中");
        String userName = (String) principalCollection.getPrimaryPrincipal();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        if(userName.equals("superAdmin")){
    
    
            //只有SuperAdmin才有S:P权限
            info.addStringPermission("s:p");
            //添加角色也可以
//            info.addRole("s");
        }else {
    
    
            info.addStringPermission("");
        }
        return info;
    }
    //实现认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    
    
        System.out.println("认证当中");
        UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
        //注意,这里假设的是查表得到的username,password,可能是加密了的
        String username = "admin";
        String password = "admin";
        if (token.getUsername().equals(username) || token.getUsername().equals("superAdmin")) {
    
    
            //这里完成密码匹配,内部会进行处理,一般情况下,获取到的password是明文,或者“自欺欺人”前端对称加密后的东西
            //所以,在controller里面,我们要在加密一下,然后,和这里从数据库里面的password进行对比
            //ByteSource credentialsSalt = ByteSource.Util.bytes("Huterox");//上面添加账号时候生成的加密盐
            //SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(username,password,credentialsSalt, getName());
            //这里我把token.getUsername()传入进去了,实际上,你可以传入任何对象,然后,接下来在授权部分获取到,这个玩意
            //进入下一步的解析
            return new SimpleAuthenticationInfo(token.getUsername(),password,"");
        }
        return null;
    }
}

权限测试

ok,现在我们的dome,代码写完了,那么接下来,我们要来看看这个具体的执行过程吧。

无权限测试

首先我们来看到的是第一个接口。

    @RequestMapping("/hello")
    public String hello(){
    
    
        return "Hello";
    }

在配置里面,我们写了这个:
在这里插入图片描述
这个玩意是不需要权限的,所以,此时我们进行一个访问:
在这里插入图片描述
一切正常。

登录测试

那么现在,我们来访问admin接口,这个接口,是需要登录才能访问的,现在不登录,进行访问。
在这里插入图片描述
此时发现这里需要进入登录页面。这里我没有写html,懒得写了,就给了个提示。

在这里,我们配置了没有登录要调用的接口,和没有授权,或者权限不够要调用的接口
在这里插入图片描述

现在,我们登录一下:
在这里插入图片描述
登录成功,那么接下来,我们再访问一下:
在这里插入图片描述
可以看到成功。

权限测试

ok,接下来,我们来看到权限测试。
现在我们去访问需要超级管理员才能访问的页面。
在这里插入图片描述
这里报错了,在终端可以看到是没有权限的错:
在这里插入图片描述

这里需要注意的是,我在接口处使用的是注解模式,如果你是在配置里面写好了:
在这里插入图片描述
那么就可以跳转到/noauthor里面。
这个时候,我们就需要使用到全局异常处理器了,拦截这些Controller的错误,这里还是在Session模式下,还不是用token的,也就是前后端分离的,所以这里这样很正常。
现在登录超级管理员:
在这里插入图片描述
可以看到一切正常:
在这里插入图片描述

前后端分离token

现在我们来用用前后端分离的,这里的话,我们需要做的就是结合jwt,然后进行处理了,操作和security是类似的,其实。

我们要做的其实就是在基础上集成JWT。

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.2.0</version>
</dependency>

然后的话,我们修改一下过滤器。
这里JWT是啥,怎么用,后面怎么用我就不说了,这个需要结合你实际的项目,而且默认你是有基础的,只是想要玩玩shiro,而已。

JWTFilter

@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {
    
    
    // 如果请求头带有token,则对token进行检查;否则,直接放行
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
    
    
        // 判断请求头是否带有 token
        if (isLoginAttempt(request, response)) {
    
    
            // 如果存在 token ,则进入executeLogin()方法执行登入,并检测 token 的正确性
            try {
    
    
                executeLogin(request, response);
            } catch (Exception e) {
    
    
                log.error("Error! {}", e.getMessage());
                responseError(response, e.getMessage());
            }
        }
        // 如果不存在 token ,则可能是执行登录操作/游客访问状态,所以直接放行
        return true;
    }

    // 检测 header中是否包含 token
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
    
    
        return getTokenFromRequest(request) != null;
    }

   // 执行登入操作
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
    
    
        String token = getTokenFromRequest(request);
        JwtToken jwtToken = new JwtToken(token);
        // 提交给 realm 进行登入,如果错误,会抛出异常并捕获
        getSubject(request, response).login(jwtToken);
        // 如果没有抛出异常,则代表登入成功,返回 true
        return true;
    }

    // 从请求中获取 token
    private String getTokenFromRequest(ServletRequest request) {
    
    
        HttpServletRequest req = (HttpServletRequest) request;
        return req.getHeader("Token");
    }

    // 非法请求将跳转到 "/unauthorized/**"
    private void responseError(ServletResponse response, String message) {
    
    
        try {
    
    
            HttpServletResponse resp = (HttpServletResponse) response;
            // 设置编码,否则中文字符在重定向时会变为空字符串
            message = URLEncoder.encode(message, "UTF-8");
            resp.sendRedirect("/noauthori/" + message);
        } catch (UnsupportedEncodingException e) {
    
    
            log.error("Error! {}", e.getMessage());
        } catch (IOException e) {
    
    
            log.error("Error! {}", e.getMessage());
        }
    }
}

这里的话,我们还可以再对JwtToken封装一下,方便后面拿东西。

public class JwtToken implements AuthenticationToken {
    
    
    private String token;
    
    public JwtToken(String token) {
    
    
        this.token = token;
    }
    
    @Override
    public Object getPrincipal() {
    
    
        return token;
    }
    @Override
    public Object getCredentials() {
    
    
        return token;
    }
}

重写认证

 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    
    
       
        // 这里的 token从 JWTFilter 的 executeLogin() 方法传递过来,先前我们封装了jwttoken
        //如果验证通过,我们把这个JwtToken往下传递了
        String token = (String) authenticationToken.getCredentials();
     	//然后这里还是查表那一套

        return new SimpleAuthenticationInfo(token, token, getName());
    }

同样的授权也是一样的。

那么之后的话,我们的流程就是,登录完之后,前端拿到token,我们设置需要验证的地方,就会通过我们的过滤器,然后执行这一套逻辑。

修改配置

最后我们重新修改配置:


@Configuration
public class ShiroConfig {
    
    

    //ShiroFilter过滤所有请求
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
    
    
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //给ShiroFilter配置安全管理器
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //配置那些需要放行,需要拦截,需要怎么怎么样
        /*这里有对应的注解
        *anon:无需认证就可以访问
        *authc:必须认证了才能让问
        *user:必须拥有记住我功能才能用
        *perms:拥有对某个资源的权限才能访问[角色:操作];
        *roLe:拥有某个角色权限才能访问[角色]

        * @RequiresAuthentication:必须经过认证才能访问
          @RequiresUser:必须经过认证,并且有记住我功能才能访问
          @RequiresPermissions("permission:operation"):需要拥有指定权限才能访问,其中permission为资源名,operation为操作名
          @RequiresRoles("roleName"):需要拥有指定角色(roleName)才能访问
        * */

        // 设置自定义的拦截器
        Map<String, Filter> filterMap = new LinkedHashMap<>();
        filterMap.put("jwt", new JwtFilter());
        shiroFilterFactoryBean.setFilters(filterMap);

        Map<String, String> map = new HashMap<String, String>();
        map.put("/hello","anon");
        map.put("/admin","authc");
//        map.put("/superAdmin","perms[s:p]");
        //去登陆接口,没有通过验证进入
        shiroFilterFactoryBean.setLoginUrl("/toLogin");
        shiroFilterFactoryBean.setUnauthorizedUrl("/noauthor");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }

    //创建安全管理器
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm) {
    
    
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm);
        // 关闭 shiro 自带的 session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator evaluator = new DefaultSessionStorageEvaluator();
        evaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(evaluator);
        securityManager.setSubjectDAO(subjectDAO);
        return securityManager;
    }
    //创建自定义Realm
    @Bean
    public Realm getRealm() {
    
    
        CustomerRealm realm = new CustomerRealm();
        return realm;
    }


    // 对Shiro注解的支持
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
    
    
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        creator.setProxyTargetClass(true);
        return creator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
    
    
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;

    }


}


总结

okey,这些就是全部内容了。没啥东西其实,就是简单换个脑子,过过。

猜你喜欢

转载自blog.csdn.net/FUTEROX/article/details/131225661