SpringMVC整合shiro、自定义sessionManager实现前后端分离

前端从后端剥离,形成一个前端工程,前端只利用Json来和后端进行交互,后端不返回页面,只返回Json数据。前后端之间完全通过public API约定。

1 自定义Realms

Shiro从Realm获取安全数据(如用户、角色、权限):就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。
doGetAuthorizationInfo()方法用于控制用户权限获取。
doGetAuthenticationInfo()方法用于控制用户登录。
在项目包下建一个ShiroRealm类,继承AuthorizingRealm抽象类。

public class ShiroRealm extends AuthorizingRealm {
    private static final Logger logger = LoggerFactory.getLogger(ShiroRealm.class);
    @Autowired
    MemberService memberService;

    @Autowired
    PermissionService permissionService;
    /**
     * 获取身份信息,我们可以在这个方法中,从数据库获取该用户的权限和角色信息
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        ShiroUser shiroUser = (ShiroUser) principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        Set<String> roles = Sets.newHashSet();
        Set<String> permissions = Sets.newHashSet();
        List<Integer> pIdList =null;
        try
        {
            pIdList = permissionService.findPermissionIdByMemberId(UserUtils.getCurrrentUserId());
        }catch(Exception e)
        {
            e.printStackTrace();
        }

        if (!GeneralUtil.isEmpty(pIdList))
        {
            for (int pid: pIdList)
            {
                permissions.add(pid+"");
            }
        }
        authorizationInfo.setRoles(roles);
        authorizationInfo.setStringPermissions(permissions);
        return authorizationInfo;
    }
    
    /**
     * 在这个方法中,进行身份验证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        
        //CaptchaUsernamePasswordToken token = (CaptchaUsernamePasswordToken) authenticationToken;
        String username = (String) authenticationToken.getPrincipal();
        String captcha = null;
        Object obj_captcha = SecurityUtils.getSubject().getSession()
                .getAttribute("captchaCode");

        
        String ip = null;
        Object obj_ip = SecurityUtils.getSubject().getSession()
                .getAttribute("captchaCodeIp");
        if (obj_ip instanceof String) {
            ip = (String) obj_ip;
        }
        
        Member member =this.memberService.findUserByUserLoginName(username);
        
        if (member == null) {
            return null;
        }
       
        return new SimpleAuthenticationInfo(
                //new ShiroUser(user.getId(), user.getLoginName(), user.getLoginName()),
        		new ShiroUser(member.getId().longValue(), member.getUserName(), member.getUserName()),
        		
                member.getPassword(), //密码
                new SimpleByteSource(member.getUserName()),//salt=username
                getName()  //realm name
        );
    }
     }

2Shiro的配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:util="http://www.springframework.org/schema/util" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">

    <!-- 缓存管理器 使用Ehcache实现 -->
    <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
    </bean>

    <!-- 凭证匹配器 -->
    <bean id="credentialsMatcher"
          class="cn.suyan.shiro.credentials.RetryLimitHashedCredentialsMatcher">
        <constructor-arg ref="cacheManager"/>
        <property name="hashAlgorithmName" value="md5"/>
        <property name="hashIterations" value="2"/>
        <property name="storedCredentialsHexEncoded" value="true"/>
    </bean>

    <!-- Realm实现 -->
    <bean id="shiroRealm" class="cn.suyan.shiro.realm.ShiroRealm">
        <property name="credentialsMatcher" ref="credentialsMatcher"/>
        <property name="cachingEnabled" value="true"/>
        <property name="authenticationCachingEnabled" value="true"/>
        <property name="authenticationCacheName" value="authenticationCache"/>
        <property name="authorizationCachingEnabled" value="true"/>
        <property name="authorizationCacheName" value="authorizationCache"/>
    </bean>
    <!-- 会话ID生成器 -->
    <bean id="sessionIdGenerator" class="cn.suyan.shiro.session.UuIdSessionIdGenerator"/>

    <!-- 会话Cookie模板 -->
    <bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
        <constructor-arg value="sid"/>
        <property name="httpOnly" value="true"/>
        <property name="maxAge" value="-1"/>
    </bean>

    <bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
        <constructor-arg value="rememberMe"/>
        <property name="httpOnly" value="true"/>
        <property name="maxAge" value="2592000"/><!-- 30天 -->
    </bean>

    <!-- rememberMe管理器 -->
    <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
        <!-- rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位) -->
        <property name="cipherKey"
                  value="#{T(org.apache.shiro.codec.Base64).decode('4AvVhmFLUs0KTA3Kprsdag==')}"/>
        <property name="cookie" ref="rememberMeCookie"/>
    </bean>

    <!-- 会话DAO -->
    <bean id="sessionDAO"
          class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO">
        <property name="activeSessionsCacheName" value="shiro-activeSessionCache"/>
        <property name="sessionIdGenerator" ref="sessionIdGenerator"/>
    </bean>

    <!-- 会话验证调度器 -->
    <bean id="sessionValidationScheduler"
          class="org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler">
        <property name="sessionValidationInterval" value="1800000"/>
        <property name="sessionManager" ref="sessionManager"/>
    </bean>

    <!-- 会话管理器 -->
    <bean id="sessionManager"
          class="cn.suyan.shiro.session.CustomSessionManager">
        <property name="globalSessionTimeout" value="1800000"/>
        <property name="deleteInvalidSessions" value="true"/>
        <property name="sessionValidationSchedulerEnabled" value="true"/>
        <property name="sessionValidationScheduler" ref="sessionValidationScheduler"/>
        <property name="sessionDAO" ref="sessionDAO"/>
        <property name="sessionIdCookieEnabled" value="true"/>
        <property name="sessionIdCookie" ref="sessionIdCookie"/>
    </bean>

    <!-- 安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realms">
            <list>
                <ref bean="shiroRealm"/>
            </list>
        </property>
        <property name="cacheManager" ref="cacheManager"/>
        <property name="sessionManager" ref="sessionManager"/>
        <property name="rememberMeManager" ref="rememberMeManager"/>
    </bean>
    <!-- 相当于调用SecurityUtils.setSecurityManager(securityManager) -->
    <bean
            class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
        <property name="staticMethod"
                  value="org.apache.shiro.SecurityUtils.setSecurityManager"/>
        <property name="arguments" ref="securityManager"/>
    </bean>


    <bean id="userFilter" class="cn.suyan.shiro.filter.SysUserFilter"/>
    <!--退出跳转拦截-->
    <!--<bean id="logoutFilter" class="org.apache.shiro.web.filter.authc.LogoutFilter">-->
        <!--<property name="redirectUrl" value="/login"/>-->
    <!--</bean>-->
    <bean id="logoutFilter" class="cn.suyan.shiro.filter.ForceLogoutFilter">
    </bean>

    <!-- Shiro的Web过滤器 -->
    <bean id="shiroFilter" class="cn.suyan.shiro.PlatformShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="/unauth"/>
        <property name="unauthorizedUrl" value="/unauth"/>
        <property name="filters">
            <util:map>
                <entry key="logout" value-ref="logoutFilter"/>
                <entry key="user" value-ref="userFilter"/>
            </util:map>
        </property>
        <property name="filterChainDefinitions">
            <value>
                <!--跳转视图忽略权限-->
                /view/** = anon
                <!---->
                /unauth=anon
                /login=anon
                /logout = logout
                /api/** = anon
                /error = anon
                /resources/** = anon
                /validateTicket = anon
                /** = user
                <!--/** = ssl -->
            </value>
        </property>
    </bean>

    <!-- Shiro生命周期处理器 -->
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
</beans>

3修改我们的Controller中的登录请求

@RequestMapping(value = "login", method = RequestMethod.POST)
    @ResponseBody
    public ResultBean userLogin(HttpServletRequest request, @RequestBody UserBean bean) {
        JSONObject jsonObject = new JSONObject();
        ResultBean resultBean=new ResultBean();

        String username=bean.getUsername();
        String password=bean.getPassword();
        logger.info(" 我进来了");
        logger.debug("scheme is {}", request.getScheme());
        if (null != SecurityUtils.getSubject().getPrincipal()) {
            SecurityUtils.getSubject().logout();
        }
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        try {
            subject.login(token);
            jsonObject.put("token", subject.getSession().getId());
            LoginInfo loginInfo=new LoginInfo();
            loginInfo.setLoginName(username);
            loginInfo.setLoingIp(request.getRemoteAddr());
            loginInfo.setOperatingResults("0");
            loginInfo.setLoginTime(new Date());
            loginInfo.setLoginStatus("1");
            this.loginInfoService.save(loginInfo);
        } catch (IncorrectCredentialsException e) {
            CommonBeanUtil.makeFailMessage(resultBean);
            jsonObject.put("msg", "密码错误");
        } catch (LockedAccountException e) {
            CommonBeanUtil.makeFailMessage(resultBean);
            jsonObject.put("msg", "登录失败,该用户已被冻结");
        } catch (AuthenticationException e) {
            CommonBeanUtil.makeFailMessage(resultBean);
            jsonObject.put("msg", "该用户不存在");
        } catch (Exception e) {
            e.printStackTrace();
        }

        resultBean.setResult(jsonObject);
        logger.info("response data:"+jsonObject.toString());
        return resultBean;
    }
  @RequestMapping(value = "/unauth")
    @ResponseBody
    public ResultBean unauth() {
        ResultBean resultBean=new ResultBean();
        CommonBeanUtil.makeFailMessage(resultBean);
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("msg", "请重新登录");
        resultBean.setResult(jsonObject);
        return resultBean;
    }

4 自定义退出拦截器

shiro框架实现了退出方法,然后跳转到登录页面,这个在前后端分离中就不太合适了,需要修改

public class ForceLogoutFilter extends AccessControlFilter {
    private static final Logger log = LoggerFactory.getLogger(ForceLogoutFilter.class);

    @Autowired
    private LoginInfoServiceImpl loginInfoService;

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        Session session = getSubject(request, response).getSession(false);
        String id=session.getId().toString();
        if(session == null) {
            return true;
        }
        return session.getAttribute(Constants.SESSION_FORCE_LOGOUT_KEY) == null;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        try {
            //强制退出
            getSubject(request, response).logout();
        } catch (Exception e) {/*ignore exception*/}

        String loginUrl = getLoginUrl() + (getLoginUrl().contains("?") ? "&" : "?") + "forceLogout=1";
        WebUtils.issueRedirect(request, response, loginUrl);
        return false;
    }

   /**
    *
    * @Description: 自定义退出的拦截器,退出返回json
    *  shiro框架实现了退出方法,然后跳转到登录页面,这个在前后端分离中就不太合适了,需要修改
    *
    * @param: 
    * @return: 
    * @auther: chengguangbing
    * @date: 2019-03-14 10:57
    */
    @Override
    public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
//        BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
//        String str = UserUtils.getCurrrentUserName();
//        String wholeStr = "";
//        while((str = reader.readLine()) != null){//一行一行的读取body体里面的内容;
//            wholeStr += str;
//        }
//        UserBean bean=JSONObject.parseObject(wholeStr,UserBean.class);
        Subject subject = getSubject(request, response);
        ResultBean resultBean=new ResultBean();
        JSONObject jsonObject = new JSONObject();
        response.setCharacterEncoding("UTF-8");
        PrintWriter out = response.getWriter();
        if(subject.isAuthenticated()){
            try {
                String username=UserUtils.getCurrrentUserName();
                subject.logout();
                LoginInfo loginInfo=new LoginInfo();
                loginInfo.setLoginName(username);
                loginInfo.setLoingIp(request.getRemoteAddr());
                loginInfo.setOperatingResults("1");
                loginInfo.setLogoutTime(new Date());
                loginInfo.setLoginStatus("0");
                this.loginInfoService.save(loginInfo);
            } catch (SessionException ise) {
                log.debug("Encountered session exception during logout.  This can generally safely be ignored.", ise);
            }
            jsonObject.put("msg", "退出成功");
        }else{
            jsonObject.put("msg", "用户没有登录,不用退出");
        }
        resultBean.setCode(WebConfig.LOGOUT_CODE);
        resultBean.setResult(jsonObject);
        out.println(JSONObject.toJSONString(resultBean));
        out.flush();
        out.close();
        return false;

    }
}

5 自定义拦截器

拦截那些未执行登录的请求,返回json格式的响应。

public class SysUserFilter extends UserFilter {
	private final static Logger LOGGER = LoggerFactory.getLogger(SysUserFilter.class);
	@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");
		PrintWriter writer = res.getWriter();
		ResultBean resultBean=new ResultBean();
		CommonBeanUtil.makeFailMessage(resultBean);
		resultBean.setCode(WebConfig.LOGOUT_CODE);
		JSONObject jsonObject = new JSONObject();
		jsonObject.put("msg", "请重新登录");
		resultBean.setResult(jsonObject);
		writer.write(JSON.toJSONString(resultBean));
		writer.close();
		return false;
	}
}

6自定义异常

经过查找发现定义的filter必须满足filter instanceof AuthorizationFilter,只有perms,roles,ssl,rest,port才是属于AuthorizationFilter,而anon,authcBasic,auchc,user是AuthenticationFilter,所以unauthorizedUrl设置后页面不跳转,解决方法要么就使用perms,roles,ssl,rest,port,要么自己配置异常处理,进行页面跳转。 这里选择自定义异常处理。处理全局异常。

public class GlobalExceptionResolver implements HandlerExceptionResolver {
	
	private final static Logger LOGGER = LoggerFactory.getLogger(cn.suyan.exceptions.GlobalExceptionResolver.class);
	
	@Override
	public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
		LOGGER.debug("Catch exception Url :{}, Method: {}, Exception: {}", request.getRequestURL(), request.getMethod(), ExceptionUtils.getMessage(ex));
		ex.printStackTrace();
		return returnJson(request, response, ex);
	}
	
	private ModelAndView parsePost(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
		if(isAjax(request)){
			return returnJson(request, response, ex);
		}else {
			return returnView(request,ex);
		}
	}
	
	public static ModelAndView returnJson(HttpServletRequest request, HttpServletResponse response, final Exception ex) {
		
		ModelAndView modelAndView = new ModelAndView();
		try {
			response.setHeader("Access-Control-Allow-Origin", "*");
			response.setStatus(HttpServletResponse.SC_OK);
			response.setCharacterEncoding("UTF-8");
			PrintWriter writer = response.getWriter();
			ResultBean resultBean=new ResultBean();
			CommonBeanUtil.makeFailMessage(resultBean);
			JSONObject jsonObject = new JSONObject();
			jsonObject.put("msg", getExMessage(ex));
			resultBean.setResult(jsonObject);
			writer.write(JSON.toJSONString(resultBean));
			writer.close();
		}catch (Exception e){

		}
		return modelAndView;
	}
	
	public static ModelAndView returnView(HttpServletRequest request, Exception ex) {
		ModelAndView modelAndView = new ModelAndView();
		StringWriter sw = new StringWriter();
		ex.printStackTrace(new PrintWriter(sw, true));
		modelAndView.setViewName("error.jsp");
		request.setAttribute("errorMsg", getExMessage(ex));
		request.setAttribute("errorprint", sw.toString());
		return modelAndView;
	}
	
	
	private static String getExMessage(Exception ex) {
		if (ex instanceof UnauthorizedException) {
			return "无权限调用";
		}
		return ex.getMessage();
	}
	
}

7自定义sessionManager

传统结构项目中,shiro从cookie中读取sessionId以此来维持会话,在前后端分离的项目中,我们选择在ajax的请求头中传递sessionId,因此需要重写shiro获取sessionId的方式。自定义ShiroSessionManager类继承DefaultWebSessionManager类,重写getSessionId方法。

public class CustomSessionManager extends DefaultWebSessionManager {

    /**
     * 获取请求头中key为“Authorization”的value == sessionId
     */
    private static final String AUTHORIZATION ="Authorization";

    private static final String REFERENCED_SESSION_ID_SOURCE = "cookie";

    /**
     *  @Description shiro框架 自定义session获取方式<br/>
     *  可自定义session获取规则。这里采用ajax请求头 {@link AUTHORIZATION}携带sessionId的方式
     */
    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        // TODO Auto-generated method stub
        String sessionId = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
        if (StringUtils.isNotEmpty(sessionId)) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return sessionId;
        }
        return super.getSessionId(request, response);
    }

}

8 ResultBean

public class ResultBean {
        private String message = WebConfig.SUCCESS_MESSAGE;//自定义常量
        private String code="";
        private String status = WebConfig.RESULT_SUCCESS;//自定义常量
        private Object result = new JSONObject();


        public String getMessage() {
            return message;
        }
        public void setMessage(String message) {
            this.message = message;
        }
        public String getCode() {
            return code;
        }
        public void setCode(String code) {
            this.code = code;
        }
        public String getStatus() {
            return status;
        }
        public void setStatus(String status) {
            this.status = status;
        }
        public Object getResult() {
            return result;
        }
        public void setResult(Object result) {
            this.result = result;
        }
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章