前言
很久之前就接触shiro了,那时候还停留在jsp,servlet阶段,后来到了ssm,web.xml里要配置好多东西。终于有一天,开启了SpringBoot的大门,前后端分离模式也就成了工作的内容。说实话,shiro原生不太支持前后端分离模式,源码里默认的登录页面是login.jsp,这就很尴尬了,不过,改一改还是能用的。本文主要讲的是如何在前后端分离的情况下使用shiro,而不是springboot下如何使用shiro的。
一、shiro的session处理
shiro的session是自定义的,和HttpServletRequest的cookie里的JSESSIONID有关。安卓不太支持cookie,所以为了能适配安卓应用以及其他无法携带cookie的请求,我们要重写shiro获取session的方法。
public class MySessionManager extends DefaultWebSessionManager {
private Logger logger = LoggerFactory.getLogger(getClass());
private static final String AUTHORIZATION = "Authorization";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
public MySessionManager() {
super();
}
/**
* @param request
* @param response
* @return
*/
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
String id=WebUtils.toHttp(request).getHeader(AUTHORIZATION);
//如果请求头中有 Authorization 则其值为sessionId
if (!StringUtils.isEmpty(id)) {//id不为空
logger.info("请求头中获取sessionId");
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
}else{
//否则按默认规则从cookie取sessionId
logger.info("默认方式获取sessionId");
return super.getSessionId(request, response);
}
}
}
先看看请求头中有没有,有的话就拿出来放到shiro的session里,没有就用默认的方法获取session。、
二、自定义登录控制
shiro是控制登录的页面的,默认登录页面为login.jsp。但是我们是前后端分离,所以后台不用管页面的跳转。shiro有几个默认的过滤器:
anon(AnonymousFilter.class),
authc(FormAuthenticationFilter.class),
authcBasic(BasicHttpAuthenticationFilter.class),
logout(LogoutFilter.class),
noSessionCreation(NoSessionCreationFilter.class),
perms(PermissionsAuthorizationFilter.class),
port(PortFilter.class),
rest(HttpMethodPermissionFilter.class),
roles(RolesAuthorizationFilter.class),
ssl(SslFilter.class),
user(UserFilter.class);
控制登录的是FormAuthenticationFilter这个类,在源码的onAccessDenied方法中,用户未登录则直接重定向到loginUrl,为了不让他重定向,我们只要继承这个类,重写它的onAccessDenied方法就可以了。
public class MyAuthenticationFilter extends FormAuthenticationFilter {
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
return super.isAccessAllowed(request, response, mappedValue);
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
Subject subject = SecurityUtils.getSubject();
Object user = subject.getPrincipal();
if (Objects.equals(user, null)) {
Map<String, Object> result = new HashMap<String, Object>();
result.put("code", 101);
result.put("msg", "未登录");
result.put("data",null);
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json");
httpServletResponse.getWriter().write(JSONObject.toJSONString(result,SerializerFeature.WriteMapNullValue));
}
return false;
}
}
这里我们是直接返回了一些字段信息,告诉前端程序员用户未登录,让他们控制页面跳转至登录页面。切记,一定要把这个自定义的登录控制过滤器加进shiro的filterChainDefinitionMap里,不然没有作用的。
然后在controller的登录方法里调用subject的login方法就可以了。
@PostMapping("/login")
public ResultBO<Object> userLogin(@RequestParam String username,
@RequestParam String password)throws Exception{
try {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
Serializable SessionId = subject.getSession().getId();
JSONObject json = new JSONObject();
json.put("token",SessionId);
subject.login(token);
return ResultTool.success(json);
}catch(LockedAccountException e){
/**账号锁定*/
return ResultTool.error(LwqExceptionEnum.USER_LOCKED);
}catch (IncorrectCredentialsException e) {
/**密码错误*/
return ResultTool.error(LwqExceptionEnum.ERROR_PASS);
} catch (UnknownAccountException e) {
/**账号不存在*/
return ResultTool.error(LwqExceptionEnum.USER_NOT_EXIT);
} /*catch (ExcessiveAttemptsException eae) {
*//**密码输入错误5次,账号锁定,一小时后再尝试*//*
return ResultTool.error(LwqExceptionEnum.WILL_LOCK);
} */
}
三、处理权限异常
shiro自带两种赋权方式,一种是页面标签,一种是controller层的注解。我们使用注解来进行赋权。但是此时会报AuthorizationException,也就是说用户无权操作,为了进行统一管理,提高用户体验,我们要和登录一样,返给前端标准的JSON格式的信息。我们使用ControllerAdvice对shiro的异常进行统一的处理。
@ControllerAdvice
@ResponseBody
public class ShiroExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(ShiroExceptionHandler.class);
@ExceptionHandler(value={AuthorizationException.class,UnauthorizedException.class})
public ResultBO<Object> unAuthorizationExceptionHandler(Exception e){
log.info("用户没有权限:{}",e.getMessage());
return ResultTool.error(LwqExceptionEnum.NO_AUTH);
}
}
ResultBO是自定义的返回数据,NO_AUTH相当于一个枚举。相当于一下代码:
Map<String, Object> result = new HashMap<String, Object>();
result.put("code", 102);
result.put("msg", "没有权限");
result.put("data",null);
return result;
四、跨域问题
前后端分离时,会导致跨域问题,可以自定义一个过滤器解决。首先在配置类中配置跨域访问路径为/**,
/**
* 跨域
*/
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("*");
}
};
}
然后写一个过滤器,设置Access-Control-Allow-Origin为所有请求路径,生产环境也可以设置为网站的域名。
@WebFilter(urlPatterns = "/*", filterName = "RestFilter")
public class RestFilter implements Filter{
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = null;
if (request instanceof HttpServletRequest) {
req = (HttpServletRequest) request;
}
HttpServletResponse res = null;
if (response instanceof HttpServletResponse) {
res = (HttpServletResponse) response;
}
if (req != null && res != null) {
//设置允许传递的参数
res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization, Requestfrom");
//设置允许带上cookie
res.setHeader("Access-Control-Allow-Credentials", "true");
String origin = Optional.ofNullable(req.getHeader("Origin")).orElse(req.getHeader("Referer"));
//设置允许的请求来源
res.setHeader("Access-Control-Allow-Origin",origin);
//设置允许的请求方法
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PATCH, PUT, DELETE, OPTIONS");
}
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
}
这里注意的是,一定要把这个过滤器加入到shiro的filterChainDefinitionMap里。
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
/**自定义的过滤器*/
Map<String,Filter> filterMap = new HashMap<>();
filterMap.put("cors",new RestFilter()); //跨域
filterMap.put("auth", new MyAuthenticationFilter());//认证
shiroFilterFactoryBean.setFilters(filterMap);
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
//退出拦截器
filterChainDefinitionMap.put("/logout", "logout");
//匿名拦截器
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/anon/**", "anon");
filterChainDefinitionMap.put("/**", "cors,auth");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
到这里,基本算是完成了。然后自定义Realm完成用户认证和授权,shiroConfig配置shiro的session等都和正常的springhboot使用shiro没有太大的区别。
写在后面的话
我觉得像是淘宝京东类的其他电商项目或者一些网站平台,不太需要shiro这种权限管理框架。首先shiro不太适用前后端分离的开发模式;其次,在分布式中,shiro就变得很鸡肋了;另外,shiro中session的过期时间对于安卓也不太友好。
就我而言,shiro对于单体项目、OA系统、ERP系统等还是有很大用处的。但是像微信、QQ等授权登录什么的,推荐SpringSecurity结合Oauth2来实现。