1. 单点
单点登录SSO(Single Sign On)是目前比较流行的企业业务整合解决方案之一。在分布式服务中,用户只需要在一处登录,即可在各个受信任的服务器之间,共享登录状态。
实现单点登录有多种方式,其区别在于是否能解决跨域登录。
实现单点登录的关键在于如何让 Session ID(或 Token)在多个域中共享。
2. 实现方式
cookie
Cookie 作用域由 domain 属性和 path 属性共同决定。
同域名不同站点
www.lymn.com/site、www.lymn.com/site2
HTTP协议天然支持同一个域名下两个站点共享cookie。
用户登录了site1,浏览器将会保存一个cookie,对应默认域名是www.lymn.com,用户访问site2,浏览器判断是同一个域名,将会在请求中加入cookie信息。
不同子域(父域 Cookie)
d1.lymn.com、d2.lymn.com
默认情况下浏览器请求时,服务器根据域名发送对应cookie。来自于d1.lymn.com的cookie默认所属域是d1.lymn.com,请求d2.lymn.com时不会发送d1cookie。
基于这种情况,用户登录d1.lymn.com后,服务端将cookie所属域名设置为.sso.com并返回,浏览器将会保存.sso.com和cookie的对应关系。当用户访问d2.lymn.com时,将会携带.sso.com对应的cookie。
专用的代理服务器
CAS
CAS 包含两个部分: CAS Server 和 CAS Client。
CAS Server 需要独立部署,主要负责对用户的认证工作;CAS Client 负责处理对客户端受保护资源的访问请求,需要登录时,重定向到 CAS Server。
TGT:Ticket Granted Ticket(俗称大令牌,或者说票根,它可以签发ST)。用户在CAS认证成功后,CAS生成cookie(叫TGC),写入浏览器,同时生成一个TGT对象,放入自己的缓存,TGT对象的ID就是cookie的值。
TGC:Ticket Granted Cookie(cookie中的value),存在Cookie中,根据它可以找到TGT。
ST:Service Ticket (小令牌),是TGT生成的,默认是用一次就生效了。
CAS特点
- 开源的企业级单点登录解决方案
- CAS Server 为需要独立部署的 Web 应用
- CAS Client 支持非常多的客户端(这里指单点登录系统中的各个 Web 应用),包括 Java, .Net, PHP, Perl, Apache, uPortal, Ruby 等。
下图是CAS 最基本协议过程:
- 当用户第一次请求cas客户端1http://localhost:8080/lymn/index,会经过AuthenticationFilter认证过滤器
- AuthenticationFilter则返回浏览器重定向地址。重定向地址就是认证服务器CAS Server登录的地址,后面的参数是我们请求的客户端地址
- 浏览器根据响应地址,发起重定向,用户登陆页面输入用户名密码,提交请求
- CAS Server 认证服务器接收用户名和密码,进行验证,根据请求参数service的值,进行重定向,其实就是回到了请求的客户端,同时会携带一个ticket令牌参数,并且在Cookie中设置一个TGC
- 客户端拿到请求中的ticket(ST)信息,经过一个ticket过滤器Cas20ProxyReceivingTicketValidationFilter,去认证系统CAS Server判断ticket是否有效
- 通过校验之后,把用户信息保存到客户端服务的session中,并把客户端服务的SessionID设置在Cookie中,同时告知客户端ticket有效。当用户再次访问该客户端,就可以根据Cookie 中的SessionID找到客户端服务的Session,获取用户信息,就不用再次进行验证了
- 当用户第二次请求cas客户端1http://localhost:8080/lymn/index,仍然会经过AuthenticationFilter过滤器,此时客户端服务session中已经存在用户的信息,浏览器中的Cookie会根据SessionID找到Session,获取用户信息,所以不需要进行验证,直接访问
- 当用户第一次请求cas客户端2http://localhost:8081/lymn/index,经过AuthenticationFilter认证过滤器
- AuthenticationFilter则返回浏览器重定向地址,去找认证中心登录
- 浏览器根据响应地址,发起重定向,因为之前访问过一次了,因此这次会携带上次返回的Cookie:TGC到认证中心
- CAS Server认证中心收到请求,发现TGC对应了一个TGT,于是用TGT签发一个ticket,并且返回给浏览器,让他重定向到http://localhost:8081/lymn/index
- 浏览器客户端带着ticket,经过一个ticket过滤器Cas20ProxyReceivingTicketValidationFilter,去认证中心验证是否有效
- 认证成功,把用户信息保存到客户端服务的session中,并把客户端服务的SessionID设置在Cookie中。当用户下次访问http://localhost:8081/lymn/index,直接访问
注:当下次访问cas Server认证系统时,浏览器将Cookie中的TGC携带到服务器,服务器根据这个TGC,查找与之对应的TGT。从而判断用户是否登录过了,是否需要展示登录页面。TGT与TGC的关系就像SESSION与Cookie中SESSIONID的关系。
全局会话与局部会话有如下约束关系
- 局部会话存在,全局会话一定存在
- 全局会话存在,局部会话不一定存在
- 全局会话销毁,局部会话必须销毁
在一个子系统中注销,sso认证中心一直监听全局会话的状态,一旦全局会话销毁,监听器将通知所有注册系统执行注销操作。
应用
CAS Server :一个war包,CAS框架已经提供。只需要把部署到web服务器上即可,主要负责对用户的认证工作。
CAS Client:开发过程中的web层, 负责处理对客户端受保护资源的访问请求,需要登录时,重定向到 CAS Server,简单配置即可。
CAS服务端配置
1)引入CAS客户端相关依赖
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-jdbc</artifactId>
<version>5.2.2</version>
</dependency>
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-jdbc-drivers</artifactId>
<version>5.2.2</version>
</dependency>
<dependency>
<groupId>com.ibm.informix</groupId>
<artifactId>jdbc</artifactId>
<version>4.10.7.20160517</version>
</dependency>
2) application.properties配置
server.context-path=/cas
server.port=8443
#开启识别json文件
cas.serviceRegistry.initFromJson=true
cas.tgc.secure=false
server.ssl.enabled=true
server.ssl.key-store=classpath:thekeystore
server.ssl.key-store-password=picc@1234
server.ssl.key-password=changeit
server.ssl.keyAlias=caskeystore
cas.logout.followServiceRedirects=true
slo.callbacks.disabled=true
#cas.authn.accept.users=casuser::Mellon
##
# CAS Thymeleaf View Configuration
#
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.cache=true
spring.thymeleaf.mode=HTML
cas.authn.jdbc.query[0].url=jdbc:informix-sqli://18.1.32.25:5955/zjk:informixserver=shanxi_fxdata;NEWCODESET=GBK,8859-1,819,Big5;IFX_USE_STRENC=true
cas.authn.jdbc.query[0].user=ccpqry
cas.authn.jdbc.query[0].password=ccpqry
cas.authn.jdbc.query[0].driverClass=com.informix.jdbc.IfxDriver
cas.authn.jdbc.query[0].fieldPassword=password
cas.authn.jdbc.query[0].sql=select * from yh_user_login where userid=? and valid='1'
cas.authn.jdbc.query[0].dialect=oorg.hibernate.dialect.Informix10Dialect
3) resources 文件夹下创建 services 文件夹,创建HTTPSandIMAPS-10000001.json,添加http
{
"@class" : "org.apereo.cas.services.RegexRegisteredService",
"serviceId" : "^(https|imaps|http)://.*",
"name" : "HTTPS and IMAPS",
"id" : 10000001,
"description" : "This service definition authorizes all application urls that support HTTPS and IMAPS protocols.",
"evaluationOrder" : 10000
}
4) resources 文件夹下创建messages_zh_CN.properties,可对这些信息进行替换
# 该用户不存在
authenticationFailure.AccountNotFoundException=\u8be5\u7528\u6237\u4e0d\u5b58\u5728
# 用户名或者密码错误
authenticationFailure.FailedLoginException=\u7528\u6237\u540d\u6216\u8005\u5bc6\u7801\u9519\u8bef
# 这个账户被禁用了
authenticationFailure.AccountDisabledException=\u8fd9\u4e2a\u8d26\u6237\u88ab\u7981\u7528\u4e86\u3002
# 这个账号锁了
authenticationFailure.AccountLockedException=\u8fd9\u4e2a\u8d26\u6237\u88ab\u4e0a\u9501\u4e86\u3002
# 密码过期了
authenticationFailure.CredentialExpiredException=\u4f60\u7684\u5bc6\u7801\u8fc7\u671f\u4e86\u3002
5)编译后部署到tomcat,配置conf.xml,添加如下内容后,启动该服务
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="150" SSLEnabled="true">
<SSLHostConfig>
<Certificate certificateKeystoreFile="D:/ca/thekeystore"
type="RSA" certificateKeystoreType="JKS" certificateKeystorePassword="picc@1234"/>
</SSLHostConfig>
</Connector>
CAS客户端配置
1)引入CAS客户端相关依赖
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
<version>3.2.2</version>
</dependency>
2)web.xml
<!-- 该过滤器用于实现单点登出功能,可选配置。 -->
<listener>
<listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
</listener>
<filter>
<filter-name>casSingleSignOutFilter</filter-name>
<filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>http://18.1.34.186/cas/</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>casSingleSignOutFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 该过滤器负责用户的认证工作,必须启用它 -->
<filter>
<filter-name>CASFilter</filter-name>
<filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
<init-param>
<param-name>casServerLoginUrl</param-name>
<param-value>http://18.1.34.186/cas/login</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<!-- 客户端地址,用于认证成功后,跳转回客户端 -->
<param-value>http://18.1.34.186</param-value>
</init-param>
<init-param>
<param-name>renew</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>gateway</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>ignorePattern</param-name>
<param-value>/css/|/js/</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CASFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 该过滤器负责对 Ticket 的校验工作,必须启用它 -->
<filter>
<filter-name>CAS Validation Filter</filter-name>
<filter-class>org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>http://18.1.34.186/cas</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<!-- 客户端地址,用于认证成功后,跳转回客户端 -->
<param-value>http://18.1.34.186</param-value>
</init-param>
<init-param>
<param-name>useSession</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>redirectAfterValidation</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Validation Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 该过滤器负责实现 HttpServletRequest 请求的包裹, 比如允许开发者通过HttpServletRequest 的 getRemoteUser()方法获得 SSO 登录用户的登录名,可选配置。 -->
<filter>
<filter-name>casHttpServletRequestWrapperFilter</filter-name>
<filter-class>org.jasig.cas.client.util.HttpServletRequestWrapperFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>casHttpServletRequestWrapperFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 该过滤器使得开发者可以通过org.jasig.cas.client.util.AssertionHolder 来获取用户的登录名。 AssertionHolder.getAssertion().getPrincipal().getName()。 -->
<filter>
<filter-name>casAssertionThreadLocalFilter</filter-name>
<filter-class>org.jasig.cas.client.util.AssertionThreadLocalFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>casAssertionThreadLocalFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
3)后台获取用户名及跨域设置
public class ToolUtil {
public static String getUserName(HttpServletRequest request){
Assertion assertion = (Assertion) request.getSession().getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION);
String userName = null;
if (assertion != null) {
AttributePrincipal principal = assertion.getPrincipal();
userName = principal.getName();
}
return userName;
}
}
@Component
public class CorsFilter implements Filter{
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) resp;
response.setHeader("Access-Control-Allow-Origin","*");
response.setHeader("Access-Control-Allow-Method","POST,GET,OPTIONS,DELETE");
response.setHeader("Access-Control-Max-Age","3600");
response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type");
response.setHeader("Access-Control-Allow-Credentials", "true");
chain.doFilter(req,resp);
}
@Override
public void init(FilterConfig arg0) throws ServletException {
}
}
CAS客户端springboot配置
1)引入CAS客户端相关依赖
<dependency>
<groupId>net.unicon.cas</groupId>
<artifactId>cas-client-autoconfig-support</artifactId>
<version>1.5.0-GA</version>
</dependency>
2) application.properties配置
#cas配置
cas.server-url-prefix=http\://18.1.34.186/cas
cas.server-login-url=http\://18.1.34.186/cas/login
cas.client-host-url=http\://18.1.34.186\:80
mycas.client-url=http\://18.1.34.186\:80/portal
cas.validation-type=CAS
3)FilterConfig配置类
@Configuration
public class FilterConfig extends CasClientConfigurerAdapter{
@Value("${cas.server-login-url}")
private String CAS_URL;
@Override
public void configureAuthenticationFilter(FilterRegistrationBean authenticationFilter) {
super.configureAuthenticationFilter(authenticationFilter);
//authenticationFilter.getInitParameters().put("authenticationRedirectStrategyClass","com.patterncat.CustomAuthRedirectStrategy");
}
@Bean
public ServletListenerRegistrationBean servletListenerRegistrationBean(){
ServletListenerRegistrationBean listenerRegistrationBean = new ServletListenerRegistrationBean();
listenerRegistrationBean.setListener(new SingleSignOutHttpSessionListener());
listenerRegistrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return listenerRegistrationBean;
}
/**
* 单点登录退出
*
* @return
*/
@Bean
public FilterRegistrationBean singleSignOutFilter() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new SingleSignOutFilter());
registrationBean.addUrlPatterns("/*");
registrationBean.addInitParameter("casServerUrlPrefix", CAS_URL);
registrationBean.setName("CAS Single Sign Out Filter");
registrationBean.setOrder(1);
return registrationBean;
}
}
4)获取用户名
public class ToolUtil {
public static String getUserName(HttpServletRequest request){
Assertion assertion = (Assertion) request.getSession().getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION);
String userName = null;
if (assertion != null) {
AttributePrincipal principal = assertion.getPrincipal();
userName = principal.getName();
}
return userName;
}
}
5)在启动类中添加cas client注解:@EnableCasClient
CAS 自定义登录验证
1)自定义登录页面,src/main/resources下创建templates,新casLoginView.html
<div class="main-body">
<div class="login-main">
<div class="login-top">
<span>单点登录系统</span>
<span class="bg1"></span>
<span class="bg2"></span>
</div>
<form class="layui-form login-bottom " id="form" th:object="${credential}" method="post" action="login">
<div class="alert alert-danger" th:if="${#fields.hasErrors('*')}">
<span th:each="err : ${#fields.errors('*')}" th:utext="${err}"></span>
</div>
<div class="center">
<div class="item">
<span class="icon icon-2"></span>
<input type="text" name="username" lay-verify="required" lay-reqtext="用户名不能为空" th:field="*{username}" placeholder="请输入登录账号" maxlength="24"/>
</div>
<div class="item">
<span class="icon icon-3"></span>
<input type="password" lay-verify="pass" id="password" name="password" th:field="*{password}" placeholder="请输入密码" maxlength="20">
<span class="bind-password icon icon-4"></span>
</div>
</div>
<div class="tip">
<a class="forget-password" target="_blank" href="pwd">修改密码</a>
</div>
<div class="layui-form-item" style="text-align:center; width:100%;height:100%;margin:0px;">
<input type="hidden" name="execution" th:value="${flowExecutionKey}"/>
<input type="hidden" name="_eventId" value="submit"/>
<input type="hidden" name="geolocation"/>
<button class="login-btn" lay-submit="" lay-filter="login">登录</button>
</div>
</form>
</div>
</div>
<div class="footer">
©版权所有 2019-2021 <a target="_blank" href="#">LYMN</a>
</div>
<script src="layui-v2.5.5/layui.js" charset="utf-8"></script>
<script>
layui.use(['form','jquery'], function () {
var $ = layui.jquery,
form = layui.form,
layer = layui.layer;
form.verify({
pass: [
/^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&.,;_+=/-])[A-Za-z\d$@$!%*#?&.,;_+=/-]{8,20}$/
,'密码不符合规则,请修改密码'
]
});
// 登录过期的时候,跳出ifram框架
if (top.location != self.location) top.location = self.location;
$('.bind-password').on('click', function () {
if ($(this).hasClass('icon-5')) {
$(this).removeClass('icon-5');
$("input[name='password']").attr('type', 'password');
} else {
$(this).addClass('icon-5');
$("input[name='password']").attr('type', 'text');
}
});
$("#password").blur(function () {
var reg = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&.,;_+=/-])[A-Za-z\d$@$!%*#?&.,;_+=/-]{8,20}$/
var password = $("#password").val();
// 用正则取匹配内容
var result = reg.test(password);
if (!result) {
layer.msg('密码不符合规则,请修改密码',{time:1*2000, icon: 2});
return false;
}
});
// 进行登录操作
form.on('submit(login)', function (data) {
data = data.field;
if (data.username == '') {
layer.msg('用户名不能为空');
return false;
}
if (data.password == '') {
layer.msg('密码不能为空');
return false;
}
$("#form").submit();
return false;
});
});
</script>
2)实体类
@Getter
@Setter
public class User implements Serializable {
private String userid;//工号
private String username;//姓名
private String epassword;//加密密码
private String salt;//加盐值
private String password;//明文密码
private Date update_date;
}
3) 引入依赖
<dependency>
<groupId>com.thetransactioncompany</groupId>
<artifactId>java-property-utils</artifactId>
<version>1.9</version>
</dependency>
<dependency>
<groupId>com.thetransactioncompany</groupId>
<artifactId>cors-filter</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-core-authentication</artifactId>
<version>5.2.2</version>
</dependency>
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-core-api-authentication</artifactId>
<version>5.2.2</version>
</dependency>
4)认证方式仅仅是传用户名和密码,实现AbstractUsernamePasswordAuthenticationHandler抽象类;提交的信息不止用户名和密码,就需继承AbstractPreAndPostProcessingAuthenticationHandler抽象类,AbstractUsernamePasswordAuthenticationHandler是继承实现的这个类,它只是用于简单用户名和密码校验。
public class CustomerHandler extends AbstractPreAndPostProcessingAuthenticationHandler {
public CustomerHandler(String name, ServicesManager servicesManager, PrincipalFactory principalFactory,
Integer order) {
super(name, servicesManager, principalFactory, order);
}
/**
* 用于判断用户的Credential(换而言之,就是登录信息),子站点的登录信息中不止有用户名密码等信息,还有部门信息的情况
*/
@Override
public boolean supports(Credential credential) {
//判断传递过来的Credential 是否是自己能处理的类型
return credential instanceof UsernamePasswordCredential;
}
/**
* 用于登录处理
*/
@Override
protected HandlerResult doAuthentication(Credential credential) throws GeneralSecurityException, PreventedException {
UsernamePasswordCredential usernamePasswordCredentia = (UsernamePasswordCredential) credential;
//获取传递过来的用户名和密码
String username = usernamePasswordCredentia.getUsername();
String password = usernamePasswordCredentia.getPassword();
// 验证用户名和密码
DriverManagerDataSource d = new DriverManagerDataSource();
d.setDriverClassName("com.informix.jdbc.IfxDriver");
d.setUrl(
"jdbc:informix-sqli://18.1.32.25:5955/zjk:informixserver=shanxi_fxdata;NEWCODESET=GBK,8859-1,819,Big5;IFX_USE_STRENC=true");
d.setUsername("ccpqry");
d.setPassword("ccpqry");
JdbcTemplate template = new JdbcTemplate();
template.setDataSource(d);
// 查询数据库加密的的密码
Map<String, Object> user = template.queryForMap("select salt,epassword from yh_user_login where userid=?",
username);
//判断加密后的输入密码是否与数据库的相同
if(ShiroUtil.decrypt(password, user.get("salt").toString(), user.get("epassword").toString())){
return createHandlerResult(credential, this.principalFactory.createPrincipal(username, Collections.emptyMap()), null);
}
throw new FailedLoginException("密码输入错误");
}
}
5)注入配置信息,继承AuthenticationEventExecutionPlanConfigurer
@Configuration
@ComponentScan("com.picc.cas.*")
@MapperScan("com.picc.cas.mapper")
public class SpringConfig implements AuthenticationEventExecutionPlanConfigurer{
@Autowired
@Qualifier("servicesManager")
private ServicesManager servicesManager;
//配置数据源
@Bean
public DataSource dataSource(){
DriverManagerDataSource dataSource= new DriverManagerDataSource("jdbc:informix-sqli://18.1.32.25:5955/zjk:informixserver=shanxi_fxdata;NEWCODESET=GBK,8859-1,819,Big5;IFX_USE_STRENC=true","ccpqry","ccpqry");
dataSource.setDriverClassName("com.informix.jdbc.IfxDriver");
return dataSource;
}
//配置 mybatis自动扫描 Mapper
@Bean
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer mapperScannerConfigurer=new MapperScannerConfigurer();
mapperScannerConfigurer.setSqlSessionFactoryBeanName("sqlSessionFactory");
mapperScannerConfigurer.setBasePackage("com.picc.cas.mapper");
return mapperScannerConfigurer;
}
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource());
ResourcePatternResolver resourcePatternResolver=new PathMatchingResourcePatternResolver();
//配置扫描对应路径的xml
factoryBean.setMapperLocations(resourcePatternResolver.getResources("classpath*:mapper/*.xml"));
factoryBean.setTypeAliasesPackage("com.picc.cas.model");
return factoryBean.getObject();
}
@Bean
public AuthenticationHandler customAuthenticationHandler() {
return new CustomerHandler("customerHandler",
servicesManager, new DefaultPrincipalFactory(), 1);
}
public void configureAuthenticationExecutionPlan(AuthenticationEventExecutionPlan plan) {
plan.registerAuthenticationHandler(customAuthenticationHandler());
}
}
6)在src/main/resources目录下新建META-INF目录,新建spring.factories文件
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.picc.cas.SpringConfig