SpringBoot整合
SpringBoot中整合Shiro和传统Spring工程是类似的,只不过一些配置的方式发生了变化。此外本篇笔记中使用的例子来自一个前后端分离的工程,这里我们也会讲解Shiro在前后端分离工程中如何使用。
引入Maven依赖
我们在SpringBoot工程中引入shiro-core、shiro-web、shiro-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基本就能以前后端分离方式正常工作了。