springboot+shiro+cas 前后端分离 知识点汇总以及遇到的坑

1.项目要接入cas服务,记录下这周的过程以及遇到的坑

    1.配置CasRealem和AuthorizingRealm的区别 

    由于上个服务 自己用的springboot+shiro 而没有整合cas,上个服务是登录后直接去库里面查询,那么何时去加载这个Ream。 我刚开始是实现了AuthorizingRealm而不是CasRealem,之后交给spring管理,结果发现怎么都进入不到自己的ream里面。最后发现是配置的问题

package com.sq.unionmanage.gateway.api;


import com.sq.unionmanage.gateway.service.common.datasource.DataSourceConfig;
import com.sq.unionmanage.gateway.service.shiro.PlatformShiroFilterFactoryBean;
import com.sq.unionmanage.gateway.service.shiro.cache.RedisCacheManager;
import com.sq.unionmanage.gateway.service.shiro.filter.ShiroFormAuthenticationFilter;
import com.sq.unionmanage.gateway.service.shiro.filter.SqUserFilter;
import com.sq.unionmanage.gateway.service.shiro.realm.ShiroRealm;
import com.sq.unionmanage.gateway.service.shiro.session.RedisSessionDAO;
import com.sq.unionmanage.gateway.service.shiro.session.UuIdSessionIdGenerator;
import com.sq.unionmanage.gateway.service.util.DESUtil;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.filter.DelegatingFilterProxy;

import javax.annotation.Resource;
import javax.servlet.Filter;
import java.util.Base64;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @Author:
 * @Date:2020/03/21
 * @Description:
 */

@Configuration
@AutoConfigureAfter(DataSourceConfig.class)
public class ShiroConfiguration {

    @Value(value = "${cms.login.url}")
    private String cmsLoginUrl;

    @Value(value = "${homepage.url}")
    private String homePageUrl;

    @Value(value = "${service.des.secret}")
    private String serviceDesSecret;

    @Value(value = "${sso.server.url}")
    private String ssoServerUrl;

    @Value(value = "${sso.login.url}")
    private String ssoLoginUrl;

    @Value(value = "${cms.server.url}")
    private String cmsServerUrl;

    //
    @Resource(name="scosSerRedisTemplate")
    private RedisTemplate scosSerRedisTemplate;


    @Value("${cms.login.url}")
    private String localLoginUrl;  //本地客户端的认证回调地址
    @Value("${service.des.secret}")
    private String desSecret;       //本地客户端的认证回调地址  的DES加密密钥

    @Bean
    public ShiroRealm shiroRealm(){
        ShiroRealm shiroRealm = new ShiroRealm();
//      /*        shiroRealm.setDefaultRoles("ROLE_USER");
        shiroRealm.setCasServerUrlPrefix(ssoServerUrl);
        //casServic的作用是 登录成功后向客户端回调
        shiroRealm.setCasService(cmsLoginUrl);
        return shiroRealm;
    }

    @Bean(name ="sessionIdGenerator")
    public UuIdSessionIdGenerator sessionIdGenerator(){
        UuIdSessionIdGenerator sessionIdGenerator = new UuIdSessionIdGenerator();
        return sessionIdGenerator;
    }

    @Bean(name = "sessionDAO")
    public RedisSessionDAO  sessionDAO(UuIdSessionIdGenerator sessionIdGenerator){
        RedisSessionDAO sessionDAO = new RedisSessionDAO();
        sessionDAO.setActiveSessionsCacheName("shiro-activeSessionCache");
        sessionDAO.setSessionIdGenerator(sessionIdGenerator);
        sessionDAO.setRedisTemplate(scosSerRedisTemplate);
        return sessionDAO;
    }

    @Bean(name = "sessionIdCookie")
    public SimpleCookie sessionIdCookie(){
        SimpleCookie sessionIdCookie = new SimpleCookie("unsid");
        sessionIdCookie.setHttpOnly(true);
        sessionIdCookie.setMaxAge(-1);
        return sessionIdCookie;
    }

    @Bean("sessionManager")
    public DefaultWebSessionManager sessionManager(RedisSessionDAO  sessionDAO, SimpleCookie sessionIdCookie){
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setGlobalSessionTimeout(1800000);
        sessionManager.setDeleteInvalidSessions(true);
        sessionManager.setSessionValidationSchedulerEnabled(true);
        //sessionManager.setSessionValidationScheduler(sessionValidationScheduler);
        sessionManager.setSessionDAO(sessionDAO);
        sessionManager.setSessionIdCookieEnabled(true);
        sessionManager.setSessionIdCookie(sessionIdCookie);
        return sessionManager;
    }

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

    //@Bean(name ="sessionValidationScheduler")
    //public QuartzSessionValidationScheduler sessionValidationScheduler(){
    //    QuartzSessionValidationScheduler sessionValidationScheduler = new QuartzSessionValidationScheduler();
    //    sessionValidationScheduler.setSessionValidationInterval(900000);
    //    sessionValidationScheduler.setSessionManager(sessionManager);
    //    return sessionValidationScheduler;
    //}




    @Bean(name="shiroCacheManager")
    public RedisCacheManager shiroCacheManager(){
        RedisCacheManager shiroCacheManager = new RedisCacheManager();
        shiroCacheManager.setRedisTemplate(scosSerRedisTemplate);
        shiroCacheManager.setExpireSeconds(1800);
        return shiroCacheManager;
    }



    @Bean(name="rememberMeManager")
    public CookieRememberMeManager rememberMeManager(SimpleCookie rememberMeCookie){
        CookieRememberMeManager rememberMeManager = new CookieRememberMeManager();
        byte[] cipherKey =  Base64.getEncoder().encode("4AvVhmFLUs0KTA3Kprsdag==".getBytes());
        rememberMeManager.setCipherKey(cipherKey);
        rememberMeManager.setCookie(rememberMeCookie);
        return rememberMeManager;
    }

    @Bean(name ="rememberMeCookie")
    public SimpleCookie rememberMeCookie(){
        SimpleCookie simpleCookie = new SimpleCookie();
        simpleCookie.setName("unionRememberMe");
        simpleCookie.setHttpOnly(true);
        simpleCookie.setMaxAge(432000);
        return simpleCookie;
    }

    @Bean
    public DefaultWebSecurityManager securityManager(RedisCacheManager shiroCacheManager,
                                                     CookieRememberMeManager rememberMeManager,
                                                     ShiroRealm shiroRealm,
                                                     DefaultWebSessionManager sessionManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(shiroRealm);

        securityManager.setCacheManager(shiroCacheManager);
        securityManager.setSessionManager(sessionManager);
        // 指定 SubjectFactory,如果要实现cas的remember me的功能,需要用到下面这个CasSubjectFactory,并设置到securityManager的subjectFactory中
        //securityManager.setSubjectFactory(new CasSubjectFactory());
        securityManager.setRememberMeManager(rememberMeManager);
        return securityManager;
    }



    @Bean("casSingleSignOutFilter")
    public SingleSignOutFilter casSingleSignOutFilter(){
        SingleSignOutFilter casSingleSignOutFilter = new SingleSignOutFilter();
        //casSingleSignOutFilter.setCasServerUrlPrefix(ssoServerUrl);
        casSingleSignOutFilter.setIgnoreInitConfiguration(true);
        return casSingleSignOutFilter;
    }


    @Bean
    public FilterRegistrationBean delegatingFilterProxy(){
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        DelegatingFilterProxy proxy = new DelegatingFilterProxy();
        proxy.setTargetFilterLifecycle(true);
        proxy.setTargetBeanName("shiroFilter");
        filterRegistrationBean.setFilter(proxy);
        return filterRegistrationBean;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    @Bean(name = "casFilter")
    public ShiroFormAuthenticationFilter casFilter() throws Exception {
        ShiroFormAuthenticationFilter casFilter = new ShiroFormAuthenticationFilter();
         casFilter.setName("casFilter");
        casFilter.setEnabled(true);
        casFilter.setFailureUrl("/unauthorized");
        //casFilter.setLoginUrl(homePageUrl);
        return casFilter;
    }

    @Bean(name = "sqUserFilter")
    public SqUserFilter sqUserFilter() throws Exception {
        SqUserFilter sqUserFilter = new SqUserFilter();
        return sqUserFilter;
    }


    @Bean(name ="shiroFilter")
    public PlatformShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager,
                                                      ShiroFormAuthenticationFilter casFilter,
                                                      SqUserFilter sqUserFilter) throws Exception {
        PlatformShiroFilterFactoryBean shiroFilterFactoryBean = new PlatformShiroFilterFactoryBean();
        Map<String , Filter> filters = new HashMap<>();
        filters.put("casFilter", casFilter);
        filters.put("sqUserFilter", sqUserFilter);
        shiroFilterFactoryBean.setFilters(filters);

         shiroFilterFactoryBean.setSecurityManager(securityManager);
        shiroFilterFactoryBean.setLoginUrl("/unauthorized");
        //shiroFilterFactoryBean.setSuccessUrl(homePageUrl);
        shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
        //shiroFilterFactoryBean.setDesSecret(serviceDesSecret);


        //注意此处使用的是LinkedHashMapU,是有顺序的,shiro会按从上到下的顺序匹配验证,匹配了就不再继续验证
        //所以上面的url要苛刻,宽松的url要放在下面,尤其是"/**"要放到最下面,如果放前面的话其后的验证规则就没作用了。
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/login", "casFilter");
        //1.不拦截的请求
        filterChainDefinitionMap.put("/unauthorized", "anon");

        //filterChainDefinitionMap.put(homePageUrl,"anon");
        filterChainDefinitionMap.put("/nginx.html", "anon");
        filterChainDefinitionMap.put("/needlogin", "anon");
        filterChainDefinitionMap.put("/logout", "anon");

        //3.需要登录 authc:该过滤器下的页面必须登录后才能访问,它是Shiro内置的一个拦截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter
        filterChainDefinitionMap.put("/mp/**", "authc");

        //4.登录过的不拦截
        filterChainDefinitionMap.put("/**", "user");






        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }
}

 

  

 

 

 

 

2.shiroFilter 注入的几个问题

  

 

 

 3.302问题

   登录成功后,shiro会通过cas回调到我的服务器地址,这个时候一般是需要我们去配置一个首页进行跳转

  

package com.sq.unionmanage.gateway.service.shiro.filter;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.sq.unionmanage.gateway.service.common.ResponseResult;
import org.apache.http.HttpStatus;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.cas.CasFilter;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Value;

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

/**
 * @Author:qxx
 * @Date:2019/9/6
 * @Description:
 */
public class ShiroFormAuthenticationFilter extends CasFilter {
    @Value(value = "${homepage.url}")
    private String homePageUrl;


    @Value(value = "${sso.login.url}")
    private String ssoLoginUrl;    //cas sso 登录页面
    @Value("${cms.login.url}")
    private String localLoginUrl;  //本地客户端的认证回调地址
    @Value("${service.des.secret}")
    private String desSecret;       //本地客户端的认证回调地址  的DES加密密钥

    /*@Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {

        return true;

    }*/

    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        System.out.println("==========executeLogin==========");
        AuthenticationToken token = createToken(request, response);
        try {
            System.out.println("========= token 是否为空===" + token);
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (token == null) {
            String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " +
                    "must be created in order to execute a login attempt.";
            throw new IllegalStateException(msg);
        }
        try {
            System.out.println("========是否登录成功===========");
            Subject subject = getSubject(request, response);
            subject.login(token);
            System.out.println("========登录成功===========");
            return onLoginSuccess(token, subject, request, response);
        } catch (AuthenticationException e) {
            return onLoginFailure(token, e, request, response);
        }

     }




    @Override
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
        HttpServletResponse rep = toHttp(response);
        System.out.println("=====onLoginSuccess=====");
        rep.setStatus(302);
        rep.setHeader("Location", homePageUrl);
        return true;
    }



    public  HttpServletRequest toHttp(ServletRequest request) {
        return (HttpServletRequest)request;
    }

    public  HttpServletResponse toHttp(ServletResponse response) {
        return (HttpServletResponse)response;
    }


}

  

4.跨域问题

   shiro登录成功后,因为源码里面有重定向,会导致header里面的信息都置为空,导致在前后端分时候出现跨域。解决办法:

   

package com.sq.unionmanage.gateway.api.web.filter;

import org.apache.http.HttpStatus;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @Author fanht
 * @Description 解决shiro 未认证后cors 跨域同源问题
 * @Date 2020/3/11 下午7:12
 * @Version 1.0
 */
@Component
@Order(1)
public class CORSFilter extends BasicHttpAuthenticationFilter{

    private static Logger logger = LoggerFactory.getLogger(AccessFilter.class);

    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;
        if(req.getMethod().equals(RequestMethod.OPTIONS.name())){
            res.setHeader("Access-control-Allow-Origin",req.getHeader("Origin"));
            res.setHeader("Access-Control-Allow-Methods","GET,POST,OPTIONS,PUT,DELETE");
            // 响应首部 Access-Control-Allow-Headers 用于 preflight request (预检请求)中,列出了将会在正式请求的 Access-Control-Expose-Headers 字段中出现的首部信息。修改为请求首部
            res.setHeader("Access-Control-Allow-Headers",req.getHeader("Access-Control-Request-Headers"));
            //给option请求直接返回正常状态
            res.setStatus(HttpStatus.SC_OK);
            return false;
        }
        return super.preHandle(request, response);
    }
}
package com.sq.unionmanage.gateway.api.web.filter;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

/**
 * @Author fanht
 * @Description
 * @Date 2020/3/24 下午8:06
 * @Version 1.0
 */
@Component
//@ConditionalOnProperty(name = "server.scheme.chanage.enabled", havingValue = "true") // 开启注解才会启动
public class RedirectFilterConfig {


    @Bean
    public FilterRegistrationBean registFilter() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new AbsoluteSendRedirectFilter());
        registration.addUrlPatterns("*");
        registration.setName("filterRegistrationBean");
        registration.setOrder(1);
        return registration;
    }

}

5.跨域问题解决后,会提示 http转https问题 解决办法

package com.sq.unionmanage.gateway.api.web.filter;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;

/**
 * @program: union-manage-gateway
 * @description:
 * @author: zjw
 * @create: 2020-03-16 16:39
 **/
@Component
//@Order(0)
public class HttpTransWrapper extends HttpServletResponseWrapper{


    private Logger logger = LoggerFactory.getLogger(this.getClass());
    private final HttpServletRequest request;

    /**
     * Constructs a response adaptor wrapping the given response.
     *
     * @param response The response to be wrapped
     * @throws IllegalArgumentException if the response is null
     */
    public HttpTransWrapper(final HttpServletRequest req, HttpServletResponse response) {
        super(response);
        this.request = req;
    }

    @Override
    public void sendRedirect(String location) throws IOException {
        if(StringUtils.isEmpty(location)){
            super.sendRedirect(location);
            return;
        }

        try {
            final URI uri = new URI(location);
            if(uri.getScheme() != null){
                super.sendRedirect(location);
                return;
            }
        } catch (URISyntaxException e) {
            logger.error("=======跳转异常========" + e);
            super.sendRedirect(location);
        }

        String finalUrl = "https://" + this.request.getServerName();
        if(request.getServerPort() != 80 && request.getServerPort() != 443 ){
            finalUrl += ":" + request.getServerPort();
        }
        finalUrl += location;
        logger.info("finalUrl:{}",finalUrl);
        if(finalUrl.indexOf("localhost") > 0){
            //todo 如果是本地测试 仍然用http的
            super.sendRedirect(location);
        }else {
            super.sendRedirect(finalUrl);
        }
    }
}
package com.sq.unionmanage.gateway.api.web.filter;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

/**
 * @Author fanht
 * @Description
 * @Date 2020/3/24 下午8:06
 * @Version 1.0
 */
@Component
//@ConditionalOnProperty(name = "server.scheme.chanage.enabled", havingValue = "true") // 开启注解才会启动
public class RedirectFilterConfig {


    @Bean
    public FilterRegistrationBean registFilter() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new AbsoluteSendRedirectFilter());
        registration.addUrlPatterns("*");
        registration.setName("filterRegistrationBean");
        registration.setOrder(1);
        return registration;
    }

}

7.登录cas回调成功后,h5cookieId为空问题,我是在回调成功后将后端的sessionId给到了H5,让他们加到session里面,原来以为这样是可以的,结果还是不行。

 

 

 

8.没有办法,最终还是通过运维来解决:大体思路是这样的: sso登录成功后,回调地址配置为前端的地址,然后当前端向后端请求时候,nginx做转发,转发到后端的ip上面,这样因为在sso转发时候cookie已经种在了前端的域名下,nginx绑定到了我们后端的ip上,相当于是 没有跨域了,也就不会出现跨域和cookie丢失的问题。然后测试发现,是可以的。 

参考:https://segmentfault.com/a/1190000015235402

https://blog.csdn.net/qq_21251983/article/details/87631991

 

posted @ 2020-03-30 08:37  Doyourself!  阅读(5069)  评论(1编辑  收藏  举报