基于SpringBoot Shiro CAS单点登录实现

来源:互联网 发布:python hdfs3 编辑:程序博客网 时间:2024/06/05 11:31

Shiro CAS 单点登录实现


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-RC3\cas-server-webapp\src\main\webapp\WEB-INF\deployerConfigContext.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-RC3\cas-server-webapp\src\main\webapp\WEB-INF\spring-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-RC3\cas-server-webapp\src\main\webapp\WEB-INF\spring-configuration\warnCookieGenerator.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-RC3\cas-server-webapp\src\main\webapp\WEB-INF\deployerConfigContext.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-RC3\cas-server-support-jdbc\src\main\java\ org\jasig\cas\adaptors\jdbc\
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-servershiro.server=http://localhost:8086/shmetro

4.3 自定义实现CasRealm

//必须是CasRealmpublic 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;@Configurationpublic 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;    }}  

五 : 单点登录验证

这里写图片描述

原创粉丝点击