SpringBoot前后端分离下使用shiro

前言

很久之前就接触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 result = new HashMap();
            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 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 unAuthorizationExceptionHandler(Exception e){
		
		log.info("用户没有权限:{}",e.getMessage());
		return ResultTool.error(LwqExceptionEnum.NO_AUTH);
	}
} 
  

ResultBO是自定义的返回数据,NO_AUTH相当于一个枚举。相当于一下代码:

Map result = new HashMap();
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 filterMap = new HashMap<>();
		filterMap.put("cors",new RestFilter()); //跨域
		filterMap.put("auth", new MyAuthenticationFilter());//认证
		shiroFilterFactoryBean.setFilters(filterMap);
		
		
		Map filterChainDefinitionMap = new LinkedHashMap();
		//退出拦截器
		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来实现。

你可能感兴趣的:(springboot)