Spring Boot+Shiro+CAS实现单点登录

在网上其实已经有很多案例,但热门主流的案例都不够全面,用起来其实还有不少问题,我记录下我在公司是如何修复这些问题的。

首先大部分网络上配置都直接写在Java里,但不利于维护,我个人比较喜欢写在配置文件里,以application.yml为例。

Maven引入三个包

<!--Apache Shiro所需的jar包 -->

<dependency>

  <groupId>org.apache.shiro</groupId>

  <artifactId>shiro-spring</artifactId>

  <version>1.2.4</version>

</dependency>

<dependency>

  <groupId>org.apache.shiro</groupId>

  <artifactId>shiro-ehcache</artifactId>

  <version>1.2.4</version>

</dependency>

<dependency>

  <groupId>org.apache.shiro</groupId>

  <artifactId>shiro-cas</artifactId>

  <version>1.2.4</version>

</dependency>

在application.yml写入配置文件

cas:

  #Cas服务器地址

  casServerUrlPrefix: http://www.cas.com:8080/cas

  #Cas登录页面地址

  casLoginUrl: ${cas.casServerUrlPrefix}/login

  #Cas登出页面地址

  casLogoutUrl: ${cas.casServerUrlPrefix}/logout

  #当前工程对外提供的服务地址

  shiroServerUrlPrefix: http://localhost:8080

  #casFilter UrlPattern

  casFilterUrlPattern: /index

  #登录地址shiroServerUrlPrefix

  loginUrl: ${cas.casLoginUrl}?service=${cas.shiroServerUrlPrefix}${cas.casFilterUrlPattern}

  #登出地址

  logoutUrl: ${cas.casServerUrlPrefix}/logout

  #登录成功跳转地址

  loginSuccessUrl: /index

  #权限认证失败跳转地址

  unauthorizedUrl: /403

然后写一个读取该配置的Java文件,建议使用静态变量,方便引用

CasProp.java

@Component
@ConfigurationProperties(prefix="cas")
public class CasProp {
    public static String casServerUrlPrefix;
    // Cas登录页面地址
    public static String casLoginUrl;
    // Cas登出页面地址
    public static String casLogoutUrl;
    // 当前工程对外提供的服务地址
    public static String shiroServerUrlPrefix;
    // casFilter UrlPattern
    public static String casFilterUrlPattern;
    // 登录地址
    public static String loginUrl;
    // 登出地址
    public static String logoutUrl;
    // 登录成功地址
    public static String loginSuccessUrl;
    // 权限认证失败跳转地址
    public static String unauthorizedUrl;

    public String getCasServerUrlPrefix() {
        return casServerUrlPrefix;
    }

    public void setCasServerUrlPrefix(String casServerUrlPrefix) {
        this.casServerUrlPrefix = casServerUrlPrefix;
    }

    public String getCasLoginUrl() {
        return casLoginUrl;
    }

    public void setCasLoginUrl(String casLoginUrl) {
        this.casLoginUrl = casLoginUrl;
    }

    public String getCasLogoutUrl() {
        return casLogoutUrl;
    }

    public void setCasLogoutUrl(String casLogoutUrl) {
        this.casLogoutUrl = casLogoutUrl;
    }

    public String getShiroServerUrlPrefix() {
        return shiroServerUrlPrefix;
    }

    public void setShiroServerUrlPrefix(String shiroServerUrlPrefix) {
        this.shiroServerUrlPrefix = shiroServerUrlPrefix;
    }

    public String getCasFilterUrlPattern() {
        return casFilterUrlPattern;
    }

    public void setCasFilterUrlPattern(String casFilterUrlPattern) {
        this.casFilterUrlPattern = casFilterUrlPattern;
    }

    public String getLoginUrl() {
        return loginUrl;
    }

    public void setLoginUrl(String loginUrl) {
        this.loginUrl = loginUrl;
    }

    public String getLogoutUrl() {
        return logoutUrl;
    }

    public void setLogoutUrl(String logoutUrl) {
        this.logoutUrl = logoutUrl;
    }

    public String getLoginSuccessUrl() {
        return loginSuccessUrl;
    }

    public void setLoginSuccessUrl(String loginSuccessUrl) {
        this.loginSuccessUrl = loginSuccessUrl;
    }

    public String getUnauthorizedUrl() {
        return unauthorizedUrl;
    }

    public void setUnauthorizedUrl(String unauthorizedUrl) {
        this.unauthorizedUrl = unauthorizedUrl;
    }
}

写一个ShiroConfig.java,这个文件是Shiro的配置文件,部分是SessionDao和redisCache,这两部份是可选加入的,不一定需要加入,看个人喜好,可选的地方均已标明,若引入时仍有错误,可酌情删除。

@Configuration
public class ShiroConfig {
    private static final Logger logger = LoggerFactory.getLogger(RoleController.class);

    //可选开始
    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.password}")
    private String password;
    @Value("${spring.redis.port}")
    private int port;
    @Value("${spring.redis.timeout}")
    private int timeout;

    @Value("${spring.cache.type}")
    private String cacheType;

    @Value("${server.session-timeout}")
    private int tomcatTimeout;
    //可选结束

    @Bean(name = "myShiroCasRealm")
    public MyShiroCasRealm myShiroCasRealm(EhCacheManager cacheManager) {
        MyShiroCasRealm realm = new MyShiroCasRealm();
        realm.setCacheManager(cacheManager);
        realm.setCasServerUrlPrefix(CasProp.casServerUrlPrefix);
        // 客户端回调地址
        realm.setCasService(CasProp.loginSuccessUrl);
        return realm;
    }


    //注册单点登出的listener
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    // 优先级需要高于Cas的Filter
    public ServletListenerRegistrationBean<?> singleSignOutHttpSessionListener() {
        ServletListenerRegistrationBean bean = new ServletListenerRegistrationBean();
        bean.setListener(new SingleSignOutHttpSessionListener());
        bean.setEnabled(true);
        return bean;
    }


    @Bean
    public FilterRegistrationBean singleSignOutFilter() {
        FilterRegistrationBean bean = new FilterRegistrationBean();
        bean.setName("singleSignOutFilter");
        bean.setFilter(new SingleSignOutFilter());
        bean.addUrlPatterns("/");
        bean.setEnabled(true);
        return bean;
    }

    /**
     *
注册DelegatingFilterProxyShiro
     *
     *
@param
    
* @return
    
* @author SHANHY
     *
@create 2016113
     */
   
@Bean
    public FilterRegistrationBean filterRegistrationBean() {
        FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
        filterRegistration.setFilter(new DelegatingFilterProxy("shiroFilter"));
        // 该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理
        filterRegistration.addInitParameter("targetFilterLifecycle", "true");
        filterRegistration.setEnabled(true);
        filterRegistration.addUrlPatterns("/*");
        return filterRegistration;
    }

    @Bean(name = "lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator daap = new DefaultAdvisorAutoProxyCreator();
        daap.setProxyTargetClass(true);
        return daap;
    }

    @Bean(name = "securityManager")
    public SecurityManager getDefaultWebSecurityManager(MyShiroCasRealm myShiroCasRealm) {
        DefaultWebSecurityManager dwsm = new DefaultWebSecurityManager();
        dwsm.setRealm(myShiroCasRealm);
        // 自定义缓存实现 使用redis,可选开始


        if (Constant.CACHE_TYPE_REDIS.equals(cacheType)) {
            dwsm.setCacheManager(rediscacheManager());
        } else {
            dwsm.setCacheManager(ehCacheManager());
            logger.info("ehCachemanager--------->");
        }

    // 自定义缓存实现 使用redis,可选结束


//   <!-- 用户授权/认证信息Cache, 采用EhCache 缓存 -->
        dwsm.setCacheManager(ehCacheManager());
        // 指定 SubjectFactory
        dwsm.setSubjectFactory(new CasSubjectFactory());
        return dwsm;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor aasa = new AuthorizationAttributeSourceAdvisor();
        aasa.setSecurityManager(securityManager);
        return aasa;
    }

    /**
     *
加载shiroFilter权限控制规则(从数据库读取然后配置)
     *
     *
@author SHANHY
     *
@create 2016114
     */

   
private void loadShiroFilterChain(ShiroFilterFactoryBean shiroFilterFactoryBean) {
        /// 下面这些规则配置最好配置到配置文件中 ///
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();

        filterChainDefinitionMap.put(CasProp.casFilterUrlPattern, "casFilter");
        filterChainDefinitionMap.put("/css/**", "anon");
        filterChainDefinitionMap.put("/js/**", "anon");
        filterChainDefinitionMap.put("/fonts/**", "anon");
        filterChainDefinitionMap.put("/img/**", "anon");
        filterChainDefinitionMap.put("/docs/**", "anon");
        filterChainDefinitionMap.put("/druid/**", "anon");
        filterChainDefinitionMap.put("/upload/**", "anon");
        filterChainDefinitionMap.put("/files/**", "anon");
        //filterChainDefinitionMap.put("/", "anon");
        filterChainDefinitionMap.put("/blog", "anon");
        filterChainDefinitionMap.put("/blog/open/**", "anon");
        filterChainDefinitionMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    }

    /**
     * CAS
过滤器
     *
     *
@return
    
* @author SHANHY
     *
@create 2016117
     */
   
@Bean(name = "casFilter")
    public CasFilter getCasFilter() {
        CasFilter casFilter = new CasFilter();
        casFilter.setName("casFilter");
        casFilter.setEnabled(true);
        // 登录失败后跳转的URL,也就是 Shiro 执行 CasRealm 的 doGetAuthenticationInfo 方法向CasServer验证tiket
        casFilter.setFailureUrl(CasProp.loginUrl);// 我们选择认证失败后再打开登录页面
        return casFilter;
    }

    /**
     * ShiroFilter
<br/>
    
* 注意这里参数中的 StudentService IScoreDao 只是一个例子,因为我们在这里可以用这样的方式获取到相关访问数据库的对象,
     * 然后读取数据库相关配置,配置到 shiroFilterFactoryBean 的访问规则中。实际项目中,请使用自己的Service来处理业务逻辑。
     *
     *
@param
    
* @param
    
* @param
    
* @return
    
* @author SHANHY
     *
@create 2016114
     */
   
@Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(SecurityManager securityManager, CasFilter casFilter) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 必须设置 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
        shiroFilterFactoryBean.setLoginUrl(CasProp.loginUrl);
        // 登录成功后要跳转的连接
//        shiroFilterFactoryBean.setSuccessUrl(CasProp.loginSuccessUrl);
        shiroFilterFactoryBean.setUnauthorizedUrl(CasProp.unauthorizedUrl);
        // 添加casFilter到shiroFilter中
        Map<String, Filter> filters = new HashMap<>();
        filters.put("casFilter", casFilter);
        shiroFilterFactoryBean.setFilters(filters);

        loadShiroFilterChain(shiroFilterFactoryBean);
        return shiroFilterFactoryBean;
    }

    @Bean
    public EhCacheManager ehCacheManager() {
        EhCacheManager em = new EhCacheManager();
        em.setCacheManager(cacheManager());
        return em;
    }

    @Bean("cacheManager2")
    CacheManager cacheManager() {
        return CacheManager.create();
    }

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

    /**
     *
配置shiro redisManager,可选
     *
     *
@return
    
*/
   
@Bean
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host);
        redisManager.setPort(port);
        redisManager.setExpire(1800);// 配置缓存过期时间
        //redisManager.setTimeout(1800);
        redisManager.setPassword(password);
        return redisManager;
    }

    /**
     * cacheManager
缓存 redis实现,可选
     * 使用的是shiro-redis开源插件
     *
     *
@return
    
*/
   
@Bean
    public RedisCacheManager rediscacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        return redisCacheManager;
    }


    /**
     * RedisSessionDAO shiro sessionDao
层的实现 通过redis,可选
     *
使用的是shiro-redis开源插件
     */
   
@Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        return redisSessionDAO;
    }
    //可选
    @Bean
    public SessionDAO sessionDAO() {
        if (Constant.CACHE_TYPE_REDIS.equals(cacheType)) {
            return redisSessionDAO();
        } else {
            return new MemorySessionDAO();
        }
    }

    /**
     * shiro session
的管理,可选
     */
   
@Bean
    public DefaultWebSessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setGlobalSessionTimeout(tomcatTimeout * 1000);
        sessionManager.setSessionDAO(sessionDAO());
        Collection<SessionListener> listeners = new ArrayList<SessionListener>();
        listeners.add(new BDSessionListener());
        sessionManager.setSessionListeners(listeners);
        return sessionManager;
    }


}

 写入MyShiroCasRealm.java,这里是用cas来做授权与认证的,酌情修改。

public class MyShiroCasRealm extends CasRealm{
    private static final Logger logger = LoggerFactory.getLogger(MyShiroCasRealm.class);
    // cas server地址
    @Value("${cas.casServerUrlPrefix}")
    public String casServerUrlPrefix;
    // 登录成功地址
    @Value("${cas.loginSuccessUrl}")
    public String loginSuccessUrl;
    @Autowired
    CasProp casProp;

    @Override
    public void setCasServerUrlPrefix(String casServerUrlPrefix) {
        this.casServerUrlPrefix = casServerUrlPrefix;
    }

    public void setLoginSuccessUrl(String loginSuccessUrl) {
        this.loginSuccessUrl = loginSuccessUrl;
    }

    @PostConstruct
    public void initProperty(){
        super.setCasServerUrlPrefix(casServerUrlPrefix);
        // 客户端回调地址
       // setCasService(loginSuccessUrl);
    }

     //这个方法用于加载权限
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) {
        UserDO users = (UserDO)super.getAvailablePrincipal(arg0);
        MenuService menuService = ApplicationContextRegister.getBean(MenuService.class);
        Set<String> perms = menuService.listPerms(users.getUserId());

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setStringPermissions(perms);
        super.onLogout(arg0);
        logger.info("info:-------------->"+perms);
        return info;
    }


    //这个方法用于认证用户身份
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        CasToken casToken = (CasToken) token;
        if (token == null) {
            return null;
        }

        String ticket = (String)casToken.getCredentials();
        if (ticket == null || ticket.trim().length()==0) {
            return null;
        }

        TicketValidator ticketValidator = ensureTicketValidator();
        try {
            // contact CAS server to validate service ticket
            String url = casProp.getShiroServerUrlPrefix() + casProp.getCasFilterUrlPattern();

           //注意这里大坑,稍后说明
            Assertion casAssertion = ticketValidator.validate(ticket,url);//"http://localhost:8080/index"
            // get principal, user id and attributes
            AttributePrincipal casPrincipal = casAssertion.getPrincipal();
            String username = casPrincipal.getName();

//            AuthenticationInfo authc = super.doGetAuthenticationInfo(token);
//            if(authc==null) return null;
//            String username = (String) authc.getPrincipals().getPrimaryPrincipal();
//            String tokens = (String) authc.getCredentials();
            Map<String, Object> map = new HashMap<>(16);
            map.put("username", username);

            UserDao userMapper = ApplicationContextRegister.getBean(UserDao.class);
            // 查询用户信息
            UserDO user = userMapper.list(map).get(0);

            // 账号不存在
            if (user == null) {
                throw new UnknownAccountException("账号或密码不正确");
            }


            // 账号锁定
            if (user.getStatus() == 0) {
                throw new LockedAccountException("账号已被锁定,请联系管理员");
            }
            SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, ticket, getName());
            logger.info("info=" + info.toString());
            return info;
        }catch (TicketValidationException e){
            e.printStackTrace();
            throw new CasAuthenticationException("Unable to validate ticket [" + ticket + "]", e);
        }


        }

常见错误

org.jasig.cas.client.validation.TicketValidationException: 票根’ST-6-wv2-gk021lK8K7FfdgqvdhSRo-org-pc’不符合目标服务 

这个错误耽误了两天最后才弄懂,网上的资料还是比较少说到,而且说到的好像也没解决这个问题,在这里我就说一下我的解决方案。

看到这个错误一般就是MyShiroCasRealm.java中的认证错误

  Assertion casAssertion = ticketValidator.validate(ticket,url);

这个方法里面的url一定要和回调的url一致才可以

例如向Cas服务器发起登录,登录地址为  http://www.cas.com:8080/cas/login?service=http://localhost:8080/index

那么这个方法里填写的的ticketValidator.validate(ticket,url)的url为service后的url,http://localhost:8080/index

  Assertion casAssertion = ticketValidator.validate(ticket,"http://localhost:8080/index");

需要这样填写才能正常识别,在网上似乎有别的说法,可能因版本不同会有别的写法,网上有人用的写法是

  Assertion casAssertion = ticketValidator.validate(ticket,"http://localhost:8080/index/");

大家需要酌情尝试!!

 

资料参考:https://blog.csdn.net/yelllowcong/article/details/79290916

  • 5
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 16
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值