基于SpringBoot 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 服务搭建
- 下载CAS服务源码: GitHub CAS
- 使用开发工具构建Maven CAS工程,博主使用IDEA工具
- 源码中有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&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包命名,这关系请求路径的问题
登录界面是这个,红框总有些红色的告警提示 和 一些国家的语言切换 ,被我干掉了 , 所以看起来比较整洁些
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;
}
}
