springboot整合Shiro实现权限控制框架(结合renrenfast分析)

Shiro是一个优秀的权限控制安全框架,核心在于认证和授权

  • 认证:你是谁?
  • 授权:你能做什么?

下面结合码云上的renren-fast源码分析Shiro在springboot项目中作为权限控制框架的应用。
        

1.引入shiro依赖

两种选择其一即可

		<-- 第一种:shiro核心依赖2-->
		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-core</artifactId>
			<version>1.4.0</version>
		</dependency>
		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-spring</artifactId>
			<version>1.4.0</version>
		</dependency>
  		 <!--  第二种:Shiro 整合 springboot实现自动化配置 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-starter</artifactId>
            <version>1.4.2</version>
        </dependency>

        

2.shiro配置(ShiroConfig)

        通常,在springboot引入一个新的框架时,都会有一个配置类针对项目的功能做配置,renrenfast在使用shiro时,主要配置了以下四个Bean

  • 1.securityManager(安全管理器)
    @Bean("securityManager")
    public SecurityManager securityManager(OAuth2Realm oAuth2Realm) {
    
        //创建DefaultWebSecurityManager 对象
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        
        //设置其使用的 Realm 为 自定义的Outh2Realm,见下文Outh2Realm类
        securityManager.setRealm(oAuth2Realm);
        
        //无需使用记住密码功能
        securityManager.setRememberMeManager(null);
        return securityManager;
    }
  • 2.shiroFilter(配置过滤器)
 @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        //创建 ShiroFilterFactoryBean 对象,用于创建 ShiroFilter过滤器
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();

        //设置上边的securityManager
        shiroFilter.setSecurityManager(securityManager);

        //创建自定义的过滤器,命名为oauth2
        Map<String, Filter> filters = new HashMap<>();
        filters.put("oauth2", new OAuth2Filter());
        shiroFilter.setFilters(filters);

        Map<String, String> filterMap = new LinkedHashMap<>();
        //列举的接口使用AnonymousFilter :允许匿名访问,即无需登陆。
        filterMap.put("/webjars/**", "anon");
        filterMap.put("/druid/**", "anon");
        filterMap.put("/app/**", "anon");
        filterMap.put("/sys/login", "anon");
        filterMap.put("/swagger/**", "anon");
        filterMap.put("/v2/api-docs", "anon");
        filterMap.put("/swagger-ui.html", "anon");
        filterMap.put("/swagger-resources/**", "anon");
        filterMap.put("/captcha.jpg", "anon");
        filterMap.put("/aaa.txt", "anon");
        //剩下的所有接口,要经过自定义过滤器的过滤
        filterMap.put("/**", "oauth2");
        shiroFilter.setFilterChainDefinitionMap(filterMap);

        return shiroFilter;
    }

        在setFilterChainDefinitionMap()方法中,里边放的是过滤器链,除了我们自定义的oauth2过滤器,还有anon过滤器,多个过滤器封装在map里,形成过滤器链,下面看一下shiro自带的过滤器有哪些?

        我们先来了解下 Shiro 内置的过滤器们。在 Shiro DefaultFilter 枚举类中,枚举了这些过滤器,以及其配置名。整理表格如下:
在这里插入图片描述
        
比较常用的过来器有:

  • anon :AnonymousFilter :允许匿名访问,即无需登陆。
  • authc :FormAuthenticationFilter :需要经过认证的用户,才可以访问。
    • 如果是匿名用户,则根据 URL 不同,会有不同的处理:
    • 如果拦截的 URL 是 GET loginUrl 登陆页面,则进行该请求,跳转到登陆页面。
    • 如果拦截的 URL 是 POST loginUrl 登陆请求,则基于请求表单的 username、password 进行认证。认证通过后,默认重定向到 GET loginSuccessUrl 地址。
    • 如果拦截的 URL 是其它 URL 时,则记录该 URL 到 Session 中。在用户登陆成功后,重定向到该 URL 上。
  • logout :LogoutFilter :拦截的 URL ,执行退出操作。退出完成后,重定向到 GET loginUrl 登陆页面。
  • roles :RolesAuthorizationFilter :拥有指定角色的用户可访问。
  • perms :PermissionsAuthorizationFilter :拥有指定权限的用户可以访问。

        
3.剩下的两个Bean对象

   //bean后置处理
    @Bean("lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     1.登录成功后, Shiro 保存用户的权限信息
     2.用户在试图请求一个带@RequiresPermissions的方法,会被AuthorizationAttributeSourceAdvisor拦截
     3.AuthorizationAttributeSourceAdvisor添加了PermissionAnnotationMethodInterceptor拦截器,判断用户是否具备权限
     4.具备权限则放行,不具备则抛出AuthorizationException
     5.当抛出AuthorizationException时,处理该异常,给用户友好提示
   */
    //权限校验
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

至此,shiroconfig配置信息完毕
        
        

3.自定义请求过滤器OAuth2Filter

方法解释如下,详情见代码!

  • createToken()和getRequestToken():主要是从请求头中获取token。
  • isAccessAllowed():判断是否允许访问,如果返回false,就进入onAccessDenied带上token进行登认证
  • onAccessDenied():携带token进行认证,核心是executeLogin()方法

public class OAuth2Filter extends AuthenticatingFilter {

    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
        //获取请求token
        String token = getRequestToken((HttpServletRequest) request);

        if(StringUtils.isBlank(token)){
            return null;
        }

        return new OAuth2Token(token);
    }

    //判断是否允许访问
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        //只允许OPTIONS类型的请求进行访问
        if(((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())){
            return true;
        }

        //如果返回false,就进入onAccessDenied带上token进行登认证,认证通过才可以访问!
        return false;
    }


    //isAccessAllowed()方法返回false,进入携带token的认证流程onAccessDenied()
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        //获取请求token
        String token = getRequestToken((HttpServletRequest) request);
        if(StringUtils.isBlank(token)){
            // 如果token不存在,设置响应头信息,并返回401
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
            httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());

            String json = new Gson().toJson(R.error(HttpStatus.SC_UNAUTHORIZED, "invalid token"));

            //返回状态码401,无效token。
            httpResponse.getWriter().print(json);

            return false;
        }

        //执行登录,执行的是基于token进行认证,
        // 方法内部实际是调用的自定义OAuth2Realm类中的doGetAuthenticationInfo(登录认证)方法!!
        return executeLogin(request, response);
    }

    //onLoginFailure()方法主要处理上一步executeLogin()方法认证失败后的json提示,
    // 逻辑和请求token不存在时的场景一致
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        //设置响应头
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setContentType("application/json;charset=utf-8");
        httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
        httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
        try {
            //处理登录失败的异常
            Throwable throwable = e.getCause() == null ? e : e.getCause();
            R r = R.error(HttpStatus.SC_UNAUTHORIZED, throwable.getMessage());

            //返回认证不通过的json信息
            String json = new Gson().toJson(r);
            httpResponse.getWriter().print(json);
        } catch (IOException e1) {

        }

        //返回false
        return false;
    }

    /**
     * 获取请求的token
     */
    private String getRequestToken(HttpServletRequest httpRequest){
        //从header中获取token
        String token = httpRequest.getHeader("token");

        //如果header中不存在token,则从参数中获取token
        if(StringUtils.isBlank(token)){
            token = httpRequest.getParameter("token");
        }

        return token;
    }


}

        
        

4.自定义认证授权(OAuth2Realm)

首先看一下Realm 的定义。
在这里插入图片描述
以上,realm最主要的职责就是“身份验证”(认证)和“授权”

看一下Realm的继承关系
在这里插入图片描述
OAuth2Realm代码

@Component
public class OAuth2Realm extends AuthorizingRealm {
    @Autowired
    private ShiroService shiroService;

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof OAuth2Token;
    }

    /**
     * 授权(验证权限时调用)
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //从认证的用户信息中获取用户对象(其实就是doGetAuthenticationInfo返回的认证对象里所包含的用户信息)
        SysUserEntity user = (SysUserEntity)principals.getPrimaryPrincipal();
        Long userId = user.getUserId();

        //用户权限列表
        Set<String> permsSet = shiroService.getUserPermissions(userId);

        //返回授权对象SimpleAuthorizationInfo,包含了这个用户所具有的权限
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setStringPermissions(permsSet);
        return info;
    }

    /**
     * 认证(登录时调用)
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String accessToken = (String) token.getPrincipal();

//        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
//        System.out.println("--------------"+usernamePasswordToken.getUsername());

        //根据accessToken,查询用户信息
        SysUserTokenEntity tokenEntity = shiroService.queryByToken(accessToken);
        //token失效
        if(tokenEntity == null || tokenEntity.getExpireTime().getTime() < System.currentTimeMillis()){
            throw new IncorrectCredentialsException("token失效,请重新登录");
        }

        //查询用户信息
        SysUserEntity user = shiroService.queryUser(tokenEntity.getUserId());
        //账号锁定
        if(user.getStatus() == 0){
            throw new LockedAccountException("账号已被锁定,请联系管理员");
        }

        //返回Shiro中的SimpleAuthenticationInfo对象,为当前用户的认证信息!
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, accessToken, getName());
        return info;
    }
}

        

5.登录授权流程

登录:

  • 1.输入用户名密码登录,并返回token
  • 2.请求先被isAccessAllowed()拦截,OPTIONS请求放行,其他请求返回false,过onAccessDenied()方法。
  • 3.带token在onAccessDenied()中认证,调用executeLogin()方法。如果认证失败调用onLoginFailure()方法返回错误信息
  • 4.executeLogin()方法中,携带token认证,认证成功,返回SimpleAuthenticationInfo对象,其中包含了用户信息

授权:

授权需要处理两个目标

  • ①:前台按钮展示
    前端页面要根据登录的用户权限展示对应权限的按钮。
    具体做法是:用户登录后,在刷新界面之前,发送请求后台接口获取登录用户和该用户的权限,然后前端遍历按钮,看该用户的权限中是否包含被遍历的按钮,把包含的按钮显示,不包含的隐藏!

    发送请求:
    在这里插入图片描述
    前端遍历:
    在这里插入图片描述

  • ②:后台接口访问权限
    此时前端按钮已经被隐藏了,但是可以通过请求路径直接访问后端接口,获得数据,我们显然不想看到这样的结果,所有还需要在后台验证权限!!

方法一:硬编码的方式,使用代码自己判断权限
在这里插入图片描述

方法二:使用shiro提供的perms过滤器,集中配置权限信息。无权限会进入ShiroFilterFactoryBean中配置的ShiroFilterFactoryBean.setUnauthorizedUrl(…)页面中去!
在这里插入图片描述

方法三:使用注解的形式
1.首先在url中有对应的权限
在这里插入图片描述

2.因为要验证权限,所以会调用到 OAuth2Realm 的 #doGetAuthorizationInfo(PrincipalCollection principals) 方法,进行鉴权,获得用户拥有的权限
3.返回SimpleAuthorizationInfo 对象,为当前用户的授权信息

6. shiro的其他功能

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

  • 5
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值