牛骨文教育服务平台(让学习变的简单)
博文笔记

基于SpringBoot Shiro CAS单点登录实现

创建时间:2017-12-14 投稿人: 浏览次数:2668

2017年12月14日12时 上海浦东 天气阴
博客状态: 已完成
进度:100%
计划完成:2017年12月16日


本文主要描述基于Spring Boot , Shiro框架的CAS单点登录实现
大致步骤如下,搭建完成后按思路写的,可能存在一些遗漏


作为一个对Shiro权限控制,SpringBoot都一知半解的程序员
突然有一天,领导要你基于SpringBoot , Shiro的二个系统做一个单点登录.

目录

  • Shiro CAS 单点登录实现
    • 目录
    • 一 简单思想概念理解
    • 二 Shiro CAS 服务搭建
      • 1 Http协议修改
      • 2 配置JDBC数据源
      • 3 自定义密码校验
    • 三 Shiro CAS 登录验证
      • 1 反编译检查
      • 2 工程名称指定
      • 3 登录登出
    • 四 Shiro CAS 与 业务系统 集成
      • 1 依赖包
      • 2 变量配置
      • 3 自定义实现CasRealm
      • 4 配置Shiro Cas对象
    • 五 单点登录验证


一 : 简单思想概念理解

1,为什么要用Shiro CAS?
Shiro权限控制易于使用,且功能齐全,Shiro既然有自己的单点登录服务,为什么还要去考虑其他的.

2,Shiro CAS单点登录流程是怎样的?
网上流程图例很多,作为一个新手,嗯?看不懂,很正常
个人简单理解 :
1,搭建CAS服务后,会有一个CAS的登录页(账号/密码),需要配置我们的数据源 和 用户名查询密码的SQL
2,之后我们在业务系统中需要配置
2.1, Shiro Cas的请求拦截过滤,将某种请求(如登录请求:login.html)重定向到这个CAS的登录页
2.2, Shiro Cas验证成功/失败后跳转地址(如成功:home.html,如失败:error.html)
2.3, 实现一个CasRealm接口的类 (MyCasRealm.java), 重写二个方法
2.4 , 其他配置不一 一说明
3, 当以上配置完成后,当你请求业务系统登录页时,会被重定向到Shiro CAS服务登录页,输入账号密码,CAS服务会基于你配置的数据源 通过 配置的SQL用用户名去查询密码,再用你输入的密码和查询的密码相比较,如果一致说明登录成功,成功之后便会调用业务系统的MyCasRealm.java类的重写方法,参数为(Token)对象(包含用户信息),你再通过Token传过来的用户名去查询出用户信息,保存到Shiro的Subject对象中,供业务系统使用,最终再跳转到你配置的成功路径。

二 : Shiro CAS 服务搭建

  1. 下载CAS服务源码: GitHub CAS
  2. 使用开发工具构建Maven CAS工程,博主使用IDEA工具
  3. 源码中有25个模块,其中只需要以下二个模块可以打包即可
    CAS WEB服务模块:cas-server-webapp
    自定义密码验证模块 : cas-server-support-jdbc
    切记使用1.7 JDK,1.8 JDK 编译有问题

打包经常会遇见些编译的问题,篇幅有限自行解决吧
遇到一些插件异常 : 注释插件,禁止控制台输出校验:注释插件
提示一下 :
如果【CAS WEB服务】编译受限 ,War包可以去网上下载
但是如果你要用到【自定义密码】则需要对 cas-server-support-jdbc 模块做修改,打包放入CAS WEB的War包中


2.1 Http协议修改

默认Https协议,不然会有其他繁琐配置.
配置:不需要HTTPS安全验证
文件 cas-4.0.0-RC3cas-server-webappsrcmainwebappWEB-INFdeployerConfigContext.xml
p:requireSecure=”false”

<bean id="proxyAuthenticationHandler"
class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler" p:httpClient-ref="httpClient" p:requireSecure="false" />

配置:不需要安全cookie
文件 cas-4.0.0-RC3cas-server-webappsrcmainwebappWEB-INFspring-configuration
ticketGrantingTicketCookieGenerator.xml
p:cookieSecure=”false”

<bean id="ticketGrantingTicketCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"
        p:cookieSecure="false"
        p:cookieMaxAge="-1"
        p:cookieName="CASTGC"
        p:cookiePath="/cas" />

配置:不需要安全cookie
文件 cas-4.0.0-RC3cas-server-webappsrcmainwebappWEB-INFspring-configurationwarnCookieGenerator.xml
p:cookieSecure=”false”

    <bean id="warnCookieGenerator"
    class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"
        p:cookieSecure="false"
        p:cookieMaxAge="-1"
        p:cookieName="CASPRIVACY"
        p:cookiePath="/cas" />

2.2 配置JDBC数据源

MYSQL数据源
文件 cas-4.0.0-RC3cas-server-webappsrcmainwebappWEB-INFdeployerConfigContext.xml
新增如下代码段

    <!--注释默认用户密码-->
    <!--自定义用户密码在这-->
    <!--<bean id="primaryAuthenticationHandler"
          class="org.jasig.cas.authentication.AcceptUsersAuthenticationHandler">
        <property name="users">
            <map>
                <entry key="casuser" value="Mellon"/>
                 <entry key="admin" value="admin"/>
            </map>
        </property>
    </bean>-->

    <!-- 设置密码的加密方式,这里使用的是MD5加密 -->
    <bean id="passwordEncoder"
          class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder"
          c:encodingAlgorithm="MD5"
          p:characterEncoding="UTF-8" />


    <!-- 自定义SQL, 自定义查询密码得SQL,默认逻辑会使用用户名查询密码,与输入密码匹配 -->
    <!-- 当你数据库存储的密码为密文的时候,默认的对比逻辑不适用,就需要改↓class中指定类的代码了,稍后说明 -->
    <bean id="primaryAuthenticationHandler"
          class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler"
          p:dataSource-ref="dataSource"
          p:passwordEncoder-ref="passwordEncoder"
          p:sql="select password from info_user where user_name = ? and del_flag=0" />

    <!-- 设置数据源 -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.
    DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
        <property name="url" value="jdbc:mysql://localhost:3306/databaseName?useUnicode=true&amp;characterEncoding=utf-8"></property>
        <property name="username" value="root"></property>
        <property name="password" value="root"></property>
    </bean>

2.3 自定义密码校验

文件 cas-4.0.0-RC3cas-server-support-jdbcsrcmainjava orgjasigcasadaptorsjdbc
QueryDatabaseAuthenticationHandler.java
该文件便是1.1自定义SQL指定的类
源码:如此简单的代码,相信你一看也就指定如何改了,结合你业务逻辑改,备注有描述,不再说明

public class QueryDatabaseAuthenticationHandler extends AbstractJdbcUsernamePasswordAuthenticationHandler {

    @NotNull
    private String sql;

    /** {@inheritDoc} */
    @Override
    protected final Principal authenticateUsernamePasswordInternal(final String username, final String password)
            throws GeneralSecurityException, PreventedException {
         //页面输入的密码经过配置的加密格式传入,密文解码原始字符
        final String encryptedPassword = this.getPasswordEncoder().encode(password);
        try {
            //this.sql为自定义查询SQL,username为查询用户密码的条件
            final String dbPassword = getJdbcTemplate().queryForObject(this.sql, String.class, username);
            //如果输入与查询不一致则抛出异常
            if (!dbPassword.equals(encryptedPassword)) {
                throw new FailedLoginException("Password does not match value on record.");
            }
        } catch (final IncorrectResultSizeDataAccessException e) {
            if (e.getActualSize() == 0) {
                throw new AccountNotFoundException(username + " not found with SQL query");
            } else {
                throw new FailedLoginException("Multiple records found for " + username);
            }
        } catch (final DataAccessException e) {
            throw new PreventedException("SQL exception while executing query for " + username, e);
        }
        //校验成功返回的对象,这个对象如果返回表示验证通过,将会调用业务系统的接口,进入自定义的CasRealm类的方法实现
        return new SimplePrincipal(username);
    }

    /**
     * @param sql The sql to set.
     */
    public void setSql(final String sql) {
        this.sql = sql;
    }
}

注意:不要忘记在Cas Web服务中加入JDBC连接驱动 和 对 cas-server-support-jdbc 的依赖

//这个jdbc的依赖,tomcat运行好像是从模块里去拉,打包的jar去公网拉?不知道什么鬼,记得多检查这个包是不是正确就好了,如果不是的话就单独对这模块打包放到CAS WEB的War包的lib中去吧
<dependency>
      <groupId>org.jasig.cas</groupId>
      <artifactId>cas-server-support-jdbc</artifactId>
      <version>${project.version}</version>
      <scope>compile</scope>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.30</version>
</dependency>

三 : Shiro CAS 登录验证

如果你是在IDEA中做修改的话,那么我们就可以很方便的通过如下方法测试了

3.1 反编译检查

记得反编译检查这个类是不是正确
这里写图片描述

3.2 工程名称指定

指定 TOMCAT 工程名称,相当于对War包命名,这关系请求路径的问题

 指定 TOMCAT 工程名称

登录界面是这个,红框总有些红色的告警提示 和 一些国家的语言切换 ,被我干掉了 , 所以看起来比较整洁些

这里写图片描述

3.3 登录/登出

输入用户密码之后,如果登录成功了,说明配置的数据源成功了
这个界面只是测试服务是否启动,数据源连接是否正确,用户密码是否正确
当然这个界面做登录页面实在丑,而你需要把你的登录页面迁过来,表单,请求不变.
登录/登出

这里写图片描述
这里写图片描述

接下来开始和业务系统联调了,如何让业务系统与Cas服务结合使用

四 : Shiro CAS 与 业务系统 集成

4.1 依赖包

<!-- 业务系统加入如下依赖包 -->
<!-- shiro 和 cas服务的版本 关系应该不大吧,我没有去找过这二者的版本关系,使用起来并没有问题  -->
<!-- shiro集成到spring boot -->
        <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.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-aspectj</artifactId>
            <version>1.2.4</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.2.4</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-cas</artifactId>
            <version>1.2.4</version>
        </dependency>

4.2 变量配置

文件 resources/application.properties
说明 这个文件应该是springboot使用的一些变量配置吧

#配置CAS服务地址 和 你业务系统地址,4.4 会使用到,上面的cas路径改cas-server了
shiro.cas=http://127.0.0.1:8080/cas-server
shiro.server=http://localhost:8086/shmetro

4.3 自定义实现CasRealm

//必须是CasRealm
public class MyRealm extends  CasRealm {

    private static Logger logger = LoggerFactory.getLogger(MyRealm.class);
    @Autowired
    private IInfoUserMapper infoUserMapper;
    @Autowired
    private IInfoAuthorityMapper infoAuthorityMapper;

    /**
     * 单Cas服务登录校验通过后,便会调用这个方法,并携带用户信息的Token参数
     * 假设只要是有Token过来,就说明是有效的登录用户,不再对密码等做校验
     * 方法名称 : doGetAuthenticationInfo
     * 功能描述 : 验证当前登陆的Subject
     * @param authcToken 当前登录用户的token
     * @return 验证信息
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) {
        AuthenticationInfo token = super.doGetAuthenticationInfo(authcToken);
        String userName =(String) token.getPrincipals().getPrimaryPrincipal();
        logger.info("当前Subject时获取到用户名为" + userName);
        //根据用户名,查找用户信息
        InfoUserBean user = loginUser(userName);
        if (user != null) {
             //user字符应该是固定写法
            SecurityUtils.getSubject().getSession().setAttribute("user", user);
        }
        //这个token返回后便会进入配置中的成功路径
        return token;
    }

    /**
     * 这里应该是请求用户的权限的方法,页面中 <shiro:hasRole name="ROLE_ADMIN"> 等类似的权限标签才会请求的方法,迁移过来业务相关代码,不解释了.
     * 方法名称 : doGetAuthorizationInfo
     * 功能描述 : 获取登录用户的权限信息
     * @param principals 登录用户信息
     * @return 用户权限信息
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        InfoUserBean user = CommonUtil.getCurrentUser();
        if (user != null) {
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
            List<AuthorityTreeBean> list;
            if (user.getId() == 1) {
                list = infoAuthorityMapper.selectAll(1,6);
            } else {
                list = infoAuthorityMapper.selectAuthorityByUserId(user.getId(), 1, 6);
            }
            List<String> permissions = new ArrayList<>();
            for (AuthorityTreeBean authority : list) {
                permissions.add(authority.getAuthCode());
            }
            info.addStringPermissions(permissions);
            return info;
        } else {
            return null;
        }
    }

    /**
     * 方法名称 : loginUser
     * 功能描述 : 登陆用户信息
     * @param userName 用户名
     * @return 用户信息
     */
    public InfoUserBean loginUser(String userName) {
        //查询用户信息
        InfoUserBean userBean = infoUserMapper.selectByName(userName);
        String pass = userBean.getPassword();
        //这里是对数据库提取的密码做加密操作,业务逻辑不必深究
        Object[] result = DataConvertUtil.getPassAndSaltByte(userBean.getPassword());
        String passwordHexStr = Hex.encodeHexString((byte[]) result[1]);
        userBean.setPassword(passwordHexStr);
        return userBean;
    }
}

4.4 配置Shiro Cas对象

之前用SpringMVC框架,Bean的配置都是在XML中, SpringBoot建议这种方式,如果不是SpringBoot框架,你也可以把这些逻辑抽到Xml中去
* 你可以直接复制过去,在我注释的地方改动下即可 *

import com.shmetro.realm.MyRealm; //记住这个类,自己实现的
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.cas.CasFilter;
import org.apache.shiro.cas.CasSubjectFactory;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.authc.LogoutFilter;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.DelegatingFilterProxy;

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

@Configuration
public class ShiroConfig {

    //路径不能改
    private static final String casFilterUrlPattern = "/shiro-cas";


    @Bean
    public MyRealm getShiroRealm(@Value("${shiro.cas}") String casServerUrlPrefix,
                                 @Value("${shiro.server}") String shiroServerUrlPrefix) {
        //将MyRealm改成你自己的类,其他不动
        MyRealm casRealm = new MyRealm();
        casRealm.setCasServerUrlPrefix(casServerUrlPrefix);
        casRealm.setCasService(shiroServerUrlPrefix + casFilterUrlPattern);
        return casRealm;
    }

    @Bean
    public FilterRegistrationBean filterRegistrationBean() {
        FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
        filterRegistration.setFilter(new DelegatingFilterProxy("shiroFilter"));
        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 DefaultWebSecurityManager getDefaultWebSecurityManager(@Value("${shiro.cas}") String casServerUrlPrefix,@Value("${shiro.server}") String shiroServerUrlPrefix) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(getShiroRealm(casServerUrlPrefix,shiroServerUrlPrefix));
        securityManager.setCacheManager(new MemoryConstrainedCacheManager());
        securityManager.setSubjectFactory(new CasSubjectFactory());
        return securityManager;
    }

    //按你业务修改
    //anon表示不过滤
    //casFilter自定义过滤器:验证成功跳转地址/验证失败跳转地址
    //logout:自定义过滤器:过滤单点登录退出请求
    private void loadShiroFilterChain(ShiroFilterFactoryBean shiroFilterFactoryBean) {
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        //你需要CAS校验的请求
        filterChainDefinitionMap.put(casFilterUrlPattern, "casFilter");
        //你需要CAS校验的请求
        filterChainDefinitionMap.put("/login.html", "casFilter");
        //不需要拦截的静态文件请求
        filterChainDefinitionMap.put("/static", "anon");
        //单点登录退出请求拦截
        filterChainDefinitionMap.put("/logout","logout");
        //不过滤其他业务系统请求
        filterChainDefinitionMap.put("/templates/*", "anon");
        //不过滤其他业务系统请求
        filterChainDefinitionMap.put("/iserver/services/*", "anon");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    }

    /**
     * 定义 CAS Filter
     */
    @Bean(name = "casFilter")
    public CasFilter getCasFilter(@Value("${shiro.cas}") String casServerUrlPrefix,
                                  @Value("${shiro.server}") String shiroServerUrlPrefix) {
        CasFilter casFilter = new CasFilter();
        casFilter.setName("casFilter");
        casFilter.setEnabled(true);
        //校验失败地址,这里失败继续重定向单点登录界面
        String failUrl = casServerUrlPrefix + "/login?service=" + shiroServerUrlPrefix + casFilterUrlPattern;
        //校验成功地址,登录成功后重定向的地址
        String successUrl = shiroServerUrlPrefix + "/templates/main.jsp";
        casFilter.setFailureUrl(failUrl);
        casFilter.setSuccessUrl(successUrl);
        return casFilter;
    }

    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager,CasFilter casFilter,@Value("${shiro.cas}") String casServerUrlPrefix,@Value("${shiro.server}") String shiroServerUrlPrefix) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        String loginUrl = casServerUrlPrefix + "/login?service=" + shiroServerUrlPrefix + casFilterUrlPattern;
        shiroFilterFactoryBean.setLoginUrl(loginUrl);
        shiroFilterFactoryBean.setSuccessUrl("/");
        Map<String, Filter> filters = new HashMap<>();
        filters.put("casFilter", casFilter);
        LogoutFilter logoutFilter = new LogoutFilter();
        logoutFilter.setRedirectUrl(casServerUrlPrefix + "/logout?service=" + shiroServerUrlPrefix);
        filters.put("logout",logoutFilter);
        shiroFilterFactoryBean.setFilters(filters);
        loadShiroFilterChain(shiroFilterFactoryBean);
        return shiroFilterFactoryBean;
    }

}  

五 : 单点登录验证

这里写图片描述

声明:该文观点仅代表作者本人,牛骨文系教育信息发布平台,牛骨文仅提供信息存储空间服务。