背景:

最近公司新接了一个小项目,在下有幸承担了后台开发的所有部分。所以基于以上环境,我开始着手搭建了一个以springboot为基础的项目,其中包含了整合shiro。

开发环境:

springboot版本1.5.9

<parent>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-parent</artifactId>
     <version>1.5.9.RELEASE</version>
     <relativePath/>
</parent>

shiro版本是shiro-spring1.4.0

<dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-spring</artifactId>
      <version>1.4.0</version>
</dependency>

项目介绍:

由于上面提到项目是个小项目,所以搭建分布式或者是微服务架构都是一件费工费时出力不讨好的事情,而且自己能力有限,目前项目搭建的是单应用架构,是前后端分离,前端用的vue但不是我来写。

开始:

1.引入依赖

<dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-spring</artifactId>
      <version>1.4.0</version>
</dependency>

2.自定义realm(如果shiro的原理以及执行流程概念还模糊的建议先梳理一下shiro框架的流程,在看这篇文章会更清晰。)

realm是用来用户认证以及权限鉴定的,所以我们要继承AuthorizingRealm类,然后重写doGetAuthorizationInfo和doGetAuthenticationInfo两个方法。doGetAuthorizationInfo是进行权限认证;doGetAuthenticationInfo是用来用户认证。

代码如下:

package com.iterge.config;

import com.iterge.entity.SysPermission;
import com.iterge.entity.SysRole;
import com.iterge.entity.User;
import com.iterge.service.SysPermissionService;
import com.iterge.service.SysRoleService;
import com.iterge.service.UserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;

/**
 * @Description 自定义权限匹配和账号密码匹配
 * @Author iterge
 * @Date 2019/6/12 14:02
 */
public class MyShiroRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;
    @Autowired
    private SysRoleService sysRoleService;
    @Autowired
    private SysPermissionService sysPermissionService;
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
        System.out.println("权限认证");
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        User user = (User) principal.getPrimaryPrincipal();
        System.out.println("**********"+user.toString());
        try {
            List<SysRole> roles = sysRoleService.selectByUid(user.getUid());
            for (SysRole role : roles) {
                authorizationInfo.addRole(role.getRolename());
            }
            List<SysPermission> permissions = sysPermissionService.selectByUid(user.getUid());
            for(SysPermission permission : permissions){
                authorizationInfo.addStringPermission(permission.getPername());
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        return authorizationInfo;
    }

    /*主要是用来进行身份认证的,也就是说验证用户输入的账号和密码是否正确。*/
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //获取用户账号和密码
        System.out.println("用户认证");
        String username = (String) authenticationToken.getPrincipal();
        User user = userService.login(username);
        if(user == null){
            return null;
        }
        if (user.getStatus() == 1) { //账户冻结
            throw new LockedAccountException();
        }
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                user,//安全数据
                user.getPassword(),//
                getName()
        );
        return authenticationInfo;
    }
}

2.自定义SessionManager,为什么要自定义这个东东呢?其实不前后端分离的情况下Shiro自身提供的sessionid+cookie的机制是能满足传统单应用系统的需求的。但是注意,咱们现在是前后端分离,ajax请求时不会像传统系统那样,会记住服务器端传过去的sessionid,那我们就要想个办法怎么让每次请求都记住这个sessionid并传给后台,让后台知道,每次请求都是被认证过的同一个人,所以我们这里就采用自定义SessionManager的方式自己来管理sessionid的获取,这样前端需要做的就是每次请求,要把后端传给它的sessionid即token,放到请求头里key为Authorization,value为后台传过来的token,然后用自定义的SessionManager获取就ok了。

package com.iterge.config;


import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;

/**
 * @Description 自定义SessionManager
 * @Author iterge
 * @Date 2019/6/13 18:58
 */
public class MySessionManager extends DefaultWebSessionManager {
    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        String token = httpServletRequest.getHeader("Authorization");
        System.out.println("Authorization:"+token);
        if(!StringUtils.isEmpty(token)){
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "Stateless request");
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return token;
        }else {
            return super.getSessionId(request, response);
        }
    }
}

3.跨域配置,前后端分离,会有两个服务,一个启动后端,一个启动前端,所以要进行跨域配置。(当然如果要把前端项目作为后端的静态资源管理起来,也不需跨培配置)

package com.iterge.config;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import java.util.List;

/**
 * @Description 跨域设置
 * @Author iterge
 * @Date 2019/6/6 18:50
 * addMapping("/**") 对接口配置跨域设置 /**代表所有接口
 * allowedHeaders("*") 允许所有的请求头
 * allowedMethods("*") 允许所有的方法 也可以设置为("POST", "GET", "PUT", "OPTIONS", "DELETE")
 * allowedOrigins("*") 允许所有的域(源地址)
 */
@Configuration
@Slf4j
public class WebMvcConfig{
    /*@Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedHeaders("*")
                .allowedMethods("*")
                .allowedOrigins("http://192.168.1.163:8890")
                .allowCredentials(true);
    }*/
    private CorsConfiguration buildConfig() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        log.info("**********WebMvcConfig**********");
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.setAllowCredentials(true);
        return corsConfiguration;
    }
    /*
     * 跨域过滤器
     * @return
     */
    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", buildConfig());
        return new CorsFilter(source);
    }
}

4.自定义过滤器。这个过滤器要特殊说明下,实际生产环境中可能,有些同学不配置这个过滤器时,会出现302的错误,这里说明一下为什么。浏览器请求接口时,会先发送一个"OPTIONS"的预请求,只有这个预请求返回的状态是200时,才会进行真正的请求。如果不把这个预请求过滤一下,就会出现302的错误,所以我们要把这个"OPTIONS"方法的预请求过滤下。

package com.iterge.config;

import com.alibaba.fastjson.JSON;
import com.iterge.entity.Result;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

/**
 * @Description
 * @Author iterge
 * @Date 2019/6/14 14:28
 */
public class CORSAuthenticationFilter extends FormAuthenticationFilter {
    public CORSAuthenticationFilter() {
        super();
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if(request instanceof HttpServletRequest){
            if (((HttpServletRequest) request).getMethod().toUpperCase().equals("OPTIONS")){
                System.out.println("OPTIONS请求");
                return true;
            }
        }
        return super.isAccessAllowed(request, response, mappedValue);
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletResponse res = (HttpServletResponse) response;
        res.setHeader("Access-Control-Allow-Origin", "*");
        res.setStatus(HttpServletResponse.SC_OK);
        res.setCharacterEncoding("UTF-8");
        res.setContentType("text/html; charset=utf-8");
        PrintWriter writer = res.getWriter();
        Map map = new HashMap();
        map.put("code", Result.NOTLOGIN.getCode());
        map.put("msg", Result.NOTLOGIN.getMsg());
        writer.write(JSON.toJSONString(map));
        writer.close();
        return false;
    }
}

定义好以后在shiro的配置类中使用。

5.shiro配置:

package com.iterge.config;

import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerExceptionResolver;

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

/**
 * @Description shiro配置文件
 * @Author iterge
 * @Date 2019/6/10 10:23
 */
@Configuration
public class ShiroConfig {

    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map filterChainDefinitionMap = new LinkedHashMap();
        //注意过滤器配置顺序 不能颠倒
        //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了,登出后跳转配置的loginUrl
        //filterChainDefinitionMap.put("/logout", "logout");
        // 配置不会被拦截的链接 顺序判断
        filterChainDefinitionMap.put("/druid/**", "anon");
        filterChainDefinitionMap.put("/swagger-ui.html", "anon");
        filterChainDefinitionMap.put("/swagger-resources", "anon");
        filterChainDefinitionMap.put("/swagger-resources/configuration/security", "anon");
        filterChainDefinitionMap.put("/swagger-resources/configuration/ui", "anon");
        filterChainDefinitionMap.put("/v2/api-docs", "anon");
        filterChainDefinitionMap.put("/webjars/springfox-swagger-ui/**", "anon");
        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/login", "anon");
        //filterChainDefinitionMap.put("/**", "authc");
        filterChainDefinitionMap.put("/**", "corsAuthenticationFilter");
        //配置shiro默认登录界面地址,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据
        //shiroFilterFactoryBean.setLoginUrl("/unauth");
        //shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        //自定义过滤器
        Map<String, Filter> filterMap = new LinkedHashMap<>();
        filterMap.put("corsAuthenticationFilter", corsAuthenticationFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        return shiroFilterFactoryBean;
    }
    @Bean
    public MyShiroRealm myShiroRealm(){
         return new MyShiroRealm();
    }

    @Bean
    public SecurityManager securityManager(MyShiroRealm realm,SessionManager sessionManager){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm);
        securityManager.setSessionManager(sessionManager);
        return securityManager;
    }

    @Bean
    public SessionManager sessionManager(){
        System.out.println("******sessionManager()");
        return new MySessionManager();
    }

    public CORSAuthenticationFilter corsAuthenticationFilter(){
        return new CORSAuthenticationFilter();
    }

    /**
     * 开启shiro aop注解支持.
     * 使用代理方式;所以需要开启代码支持;
     *
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * 注册全局异常处理
     * @return
     */
    @Bean(name = "exceptionHandler")
    public HandlerExceptionResolver handlerExceptionResolver() {
        return new MyExceptionHandler();
    }
}

最后:

附上全局异常的代码:

package com.iterge.config;

import com.alibaba.fastjson.support.spring.FastJsonJsonView;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

/**
 * @Description 自定义异常
 * @Author iterge
 * @Date 2019/6/12 15:58
 */
public class MyExceptionHandler implements HandlerExceptionResolver {
    @Override
    public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception ex) {
        ModelAndView mv = new ModelAndView();
        FastJsonJsonView view = new FastJsonJsonView();
        Map<String, Object> attributes = new HashMap<String, Object>();
        if (ex instanceof UnauthenticatedException) {
            attributes.put("code", "1000001");
            attributes.put("msg", "token错误");
        } else if (ex instanceof UnauthorizedException) {
            attributes.put("code", "1000002");
            attributes.put("msg", "用户无权限");
        } else {
            attributes.put("code", "1000003");
            attributes.put("msg", ex.getMessage());
        }
        view.setAttributesMap(attributes);
        mv.setView(view);
        return mv;
    }
}

如果有什么不明白或者不足的地方欢迎提问和指出。