SpringBoot整合

SpringBoot中整合Shiro和传统Spring工程是类似的,只不过一些配置的方式发生了变化。此外本篇笔记中使用的例子来自一个前后端分离的工程,这里我们也会讲解Shiro在前后端分离工程中如何使用。

引入Maven依赖

我们在SpringBoot工程中引入shiro-coreshiro-webshiro-spring依赖。

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.9.1</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-web</artifactId>
    <version>1.9.1</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.9.1</version>
</dependency>

Shiro官方其实提供了SpringBoot Starter,但比较难用,我们这里还是以最轻量级的方式引入Shiro依赖。

警告:Shiro作为一个认证授权框架,一直是信息安全领域漏洞挖掘的关注重点,随着时间的推移上面例子中所使用的Shiro版本可能已经过时,在实际开发中我们务必选择一个没有安全漏洞的较新版本。

代码结构

我们的代码结构大致如下:

|_config
  |_ApiUserFilter // 自定义过滤器,覆盖了原本未认证时跳转登录页的逻辑,改为使用JSON输出错误信息
  |_ExceptionAdvice // 全局异常处理器,拦截未认证和未授权异常,转换为JSON错误信息
  |_ShiroConfig // Shiro框架的JavaConfig
|_realm
  |_ApplicationRealm // 自定义Realm,从数据库加载认证和授权数据
|_controller
  |_AuthController // 登入登出控制器
  |_MenuController // 其它业务控制器(受到Shiro拦截)
  |_...

ApiUserFilter

ApiUserFilter是我们自定义的过滤器,我们这里采用的是前后端分离的项目,接口调用都是采用AJAX方式,传统是Shiro框架中直接发送302重定向是很不友好的,因此我们这里自定义了过滤器,当认证不通过时以JSON形式返回相应信息。

package com.gacfox.proarc.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.gacfox.proarc.util.ApiResult;
import org.apache.shiro.web.filter.authc.UserFilter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class ApiUserFilter extends UserFilter {

    private final ObjectMapper objectMapper;

    public ApiUserFilter() {
        super();
        objectMapper = new ObjectMapper();
    }

    @Override
    protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
        if (response instanceof HttpServletResponse) {
            ApiResult<Object> apiResult = ApiResult.failure("8401", "登录状态失效");
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            httpServletResponse.setStatus(401);
            httpServletResponse.setContentType("application/json; charset=utf-8");
            httpServletResponse.getWriter().print(objectMapper.writeValueAsString(apiResult));
        } else {
            super.redirectToLogin(request, response);
        }
    }
}

代码中,我们的ApiUserFilter继承UserFilter重写了redirectToLogin方法,实现当认证不通过时以JSON形式返回相应信息的逻辑。

ShiroConfig

Java配置中和前一篇XML配置类似,只不过将XML换成了JavaConfig。

ShiroConfig.java

package com.gacfox.proarc.config;

import com.gacfox.proarc.realm.ApplicationRealm;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {

    @Autowired
    private ApplicationRealm applicationRealm;

    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(applicationRealm);
        return securityManager;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
        filters.put("authc", new ApiUserFilter());

        Map<String, String> filterMap = new HashMap<>();
        filterMap.put("/admin/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
        return shiroFilterFactoryBean;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}

在JavaConfig中,我们配置了自定义Realm、SecurityManager、过滤器和注解扫描组件。注意在配置ShiroFilterFactoryBean时,我们注册了自定义过滤器ApiUserFilter

ExceptionAdvice

为了拦截Shiro的未认证和未授权报错信息,我们使用了SpringBoot中的统一异常处理ControllerAdvice组件。

package com.gacfox.proarc.config;

import com.gacfox.proarc.util.ApiResult;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

@Slf4j
@ControllerAdvice
public class ExceptionAdvice {
    @ExceptionHandler({RuntimeException.class})
    @ResponseBody
    public ApiResult<Object> processException(RuntimeException ex) {
        if (ex instanceof UnauthenticatedException) {
            return ApiResult.failure("8401", "登录状态失效");
        }
        if (ex instanceof UnauthorizedException) {
            return ApiResult.failure("8403", "未授权访问");
        }

        log.error("出现未预期异常", ex);
        return ApiResult.failure("8500", "系统异常");
    }
}

代码中,我们对Shiro异常进行了捕获,并以JSON形式输出错误信息。

AuthController

AuthController用于登入、等出和查询用户信息,该控制器接口不受Shiro过滤器的拦截。

package com.gacfox.proarc.controller.api.v1;

import com.gacfox.proarc.model.User;
import com.gacfox.proarc.service.UserService;
import com.gacfox.proarc.util.ApiResult;
import com.gacfox.proarc.vo.LoginFormVo;
import com.gacfox.proarc.vo.UserVo;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping(value = "/api/v1/auth")
public class AuthController {

    @Autowired
    private UserService userService;

    @PostMapping(value = "/login")
    public ApiResult<UserVo> login(@RequestBody @Validated LoginFormVo loginFormVo,
                                   BindingResult bindingResult) {
        if (!bindingResult.hasErrors()) {
            Subject subject = SecurityUtils.getSubject();
            UsernamePasswordToken token = new UsernamePasswordToken(loginFormVo.getUsername(), loginFormVo.getPassword());
            if ("1".equals(loginFormVo.getRememberMe())) {
                token.setRememberMe(true);
            }
            try {
                subject.login(token);
                User user = userService.getUserByUsername(loginFormVo.getUsername());
                UserVo userVo = new UserVo();
                BeanUtils.copyProperties(user, userVo);
                return ApiResult.success("操作成功", userVo);
            } catch (AuthenticationException e) {
                return ApiResult.failure("401", "用户名或密码错误,请重试");
            }
        } else {
            return ApiResult.failure("400", "数据校验异常");
        }
    }

    @RequiresAuthentication
    @GetMapping(value = "/logout")
    public ApiResult<Object> logout() {
        SecurityUtils.getSubject().logout();
        return ApiResult.success("操作成功", null);
    }

    @RequiresAuthentication
    @PostMapping(value = "/info")
    public ApiResult<UserVo> info() {
        User user = (User) SecurityUtils.getSubject().getPrincipal();
        UserVo userVo = new UserVo();
        BeanUtils.copyProperties(user, userVo);
        return ApiResult.success("操作成功", userVo);
    }

}

这样一番配置后,我们的Shiro基本就能以前后端分离方式正常工作了。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。