CAS单点登录-客户端集成(shiro、springboot、jwt、pac4j)(十)
CAS单点登录-客户端集成(shiro springboot jwt pac4j)(十)
由于我们通常在业务上会有以下的使用场景:
- 移动端通过业务系统鉴权
- 移动端免登录(登录一次以后)
解决方案:
PS:若想继续往下读,必须具备JWT的基本概念以及Pac4j的认证原理及应用场景
疑问
当然我们这章是讲JWT,那么会有以下的疑问:
- 若服务端已经接入了SSO,那么在移动端用户登录信息提交给SSO还是服务端?(毫无疑问是服务端,SSO对于移动端必须是透明的)
- 若采用无会话方式,如何获取token,服务端如何鉴权?(1.提交用户名密码到服务端,服务端把数据给到sso,sso最终返回用户数据 2. 根据用户数据创建token返回给移动端 3. 移动端登录后请求都带token给到服务端鉴权)
- 在使用token鉴权的情况下,退出如何解决?(客户端丢弃token即可)
鉴权流程
我们再讲一下整一个鉴权流程
配置要素
重点:sso必须支持rest认证方式
maven依赖
<dependency>
<groupId>io.buji</groupId>
<artifactId>buji-pac4j</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-cas</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-jwt</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-http</artifactId>
<version>2.1.0</version>
</dependency>
鉴权配置
若马上看下面的代码,估计一时半会看不懂,所以必须再讲一下整个交易过程
服务端鉴权过程有两个个角色分别为,Shiro、Pac4j,那他们的职责是什么?
Shiro:
判断当前Subject是否有权限执行该资源,所以他的核心是Realm、Filter,只有被过滤到的资源才会走到Realm
Pac4j:
1. JWTAuthenticator对token进行鉴别
2. CasRestFormClient 支持通过rest接口传入用户名密码进行对sso进行认证获取UserProfile
3. Pac4jRealm鉴权的realm
4. SubjectFactory需要调整成Pac4jSubjectFactory
ShiroConfiguration.java
/*
* 版权所有.(c)2008-2017. 卡尔科技工作室
*/
package com.carl.wolf.permission.config;
import io.buji.pac4j.filter.CallbackFilter;
import io.buji.pac4j.filter.LogoutFilter;
import io.buji.pac4j.filter.SecurityFilter;
import io.buji.pac4j.realm.Pac4jRealm;
import io.buji.pac4j.subject.Pac4jSubjectFactory;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.mgt.SubjectFactory;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.spring.web.config.AbstractShiroWebFilterConfiguration;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
import org.pac4j.cas.client.CasClient;
import org.pac4j.cas.client.rest.CasRestFormClient;
import org.pac4j.cas.config.CasConfiguration;
import org.pac4j.cas.config.CasProtocol;
import org.pac4j.core.client.Clients;
import org.pac4j.core.config.Config;
import org.pac4j.http.client.direct.ParameterClient;
import org.pac4j.jwt.config.encryption.SecretEncryptionConfiguration;
import org.pac4j.jwt.config.signature.SecretSignatureConfiguration;
import org.pac4j.jwt.credentials.authenticator.JwtAuthenticator;
import org.pac4j.jwt.profile.JwtGenerator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.Map;
/**
* 对shiro的安全配置,是对cas的登录策略进行配置
*
* @author Carl
* @date 2017/9/16
* @since 1.0.0
*/
@Configuration
public class ShiroConfiguration extends AbstractShiroWebFilterConfiguration {
@Value("#{ @environment['cas.prefixUrl'] ?: null }")
private String prefixUrl;
@Value("#{ @environment['cas.loginUrl'] ?: null }")
private String casLoginUrl;
@Value("#{ @environment['cas.callbackUrl'] ?: null }")
private String callbackUrl;
//jwt秘钥
@Value("${jwt.salt}")
private String salt;
@Bean
public Realm pac4jRealm() {
return new Pac4jRealm();
}
/**
* cas核心过滤器,把支持的client写上,filter过滤时才会处理,clients必须在casConfig.clients已经注册
*
* @return
*/
@Bean
public Filter casSecurityFilter() {
SecurityFilter filter = new SecurityFilter();
filter.setClients("CasClient,rest,jwt");
filter.setConfig(casConfig());
return filter;
}
/**
* JWT Token 生成器,对CommonProfile生成然后每次携带token访问
* @return
*/
@Bean
protected JwtGenerator jwtGenerator() {
return new JwtGenerator(new SecretSignatureConfiguration(salt), new SecretEncryptionConfiguration(salt));
}
/**
* 通过rest接口可以获取tgt,获取service ticket,甚至可以获取CasProfile
* @return
*/
@Bean
protected CasRestFormClient casRestFormClient() {
CasRestFormClient casRestFormClient = new CasRestFormClient();
casRestFormClient.setConfiguration(casConfiguration());
casRestFormClient.setName("rest");
return casRestFormClient;
}
@Bean
protected Clients clients() {
//可以设置默认client
Clients clients = new Clients();
//token校验器,可以用HeaderClient更安全
ParameterClient parameterClient = new ParameterClient("token", jwtAuthenticator());
parameterClient.setSupportGetRequest(true);
parameterClient.setName("jwt");
//支持的client全部设置进去
clients.setClients(casClient(), casRestFormClient(), parameterClient);
return clients;
}
/**
* JWT校验器,也就是目前设置的ParameterClient进行的校验器,是rest/或者前后端分离的核心校验器
* @return
*/
@Bean
protected JwtAuthenticator jwtAuthenticator() {
JwtAuthenticator jwtAuthenticator = new JwtAuthenticator();
jwtAuthenticator.addSignatureConfiguration(new SecretSignatureConfiguration(salt));
jwtAuthenticator.addEncryptionConfiguration(new SecretEncryptionConfiguration(salt));
return jwtAuthenticator;
}
@Bean
protected Config casConfig() {
Config config = new Config();
config.setClients(clients());
return config;
}
/**
* cas的基本设置,包括或url等等,rest调用协议等
* @return
*/
@Bean
public CasConfiguration casConfiguration() {
CasConfiguration casConfiguration = new CasConfiguration(casLoginUrl);
casConfiguration.setProtocol(CasProtocol.CAS30);
casConfiguration.setPrefixUrl(prefixUrl);
return casConfiguration;
}
@Bean
public CasClient casClient() {
CasClient casClient = new CasClient();
casClient.setConfiguration(casConfiguration());
casClient.setCallbackUrl(callbackUrl);
return casClient;
}
/**
* 路径过滤设置
*
* @return
*/
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition definition = new DefaultShiroFilterChainDefinition();
definition.addPathDefinition("/callback", "callbackFilter");
definition.addPathDefinition("/logout", "logoutFilter");
definition.addPathDefinition("/**", "casSecurityFilter");
return definition;
}
/**
* 由于cas代理了用户,所以必须通过cas进行创建对象
*
* @return
*/
@Bean
protected SubjectFactory subjectFactory() {
return new Pac4jSubjectFactory();
}
/**
* 对过滤器进行调整
*
* @param securityManager
* @return
*/
@Bean
protected ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
//把subject对象设为subjectFactory
((DefaultSecurityManager) securityManager).setSubjectFactory(subjectFactory());
ShiroFilterFactoryBean filterFactoryBean = super.shiroFilterFactoryBean();
filterFactoryBean.setSecurityManager(securityManager);
filterFactoryBean.setFilters(filters());
return filterFactoryBean;
}
/**
* 对shiro的过滤策略进行明确
* @return
*/
@Bean
protected Map<String, Filter> filters() {
//过滤器设置
Map<String, Filter> filters = new HashMap<>();
filters.put("casSecurityFilter", casSecurityFilter());
CallbackFilter callbackFilter = new CallbackFilter();
callbackFilter.setConfig(casConfig());
filters.put("callbackFilter", callbackFilter);
LogoutFilter logoutFilter = new LogoutFilter();
logoutFilter.setConfig(casConfig());
filters.put("logoutFilter", logoutFilter);
return filters;
}
}
token生成
@RequestMapping("/user/login")
public Object login(HttpServletRequest request, HttpServletResponse response) {
Map<String, Object> model = new HashMap<>();
J2EContext context = new J2EContext(request, response);
final ProfileManager<CasRestProfile> manager = new ProfileManager(context);
final Optional<CasRestProfile> profile = manager.get(true);
//获取ticket
TokenCredentials tokenCredentials = casRestFormClient.requestServiceTicket(serviceUrl, profile.get(), context);
//根据ticket获取用户信息
final CasProfile casProfile = casRestFormClient.validateServiceTicket(serviceUrl, tokenCredentials, context);
//生成jwt token
String token = generator.generate(casProfile);
model.put("token", token);
return new HttpEntity<>(model);
}
由于以上代码仅做为学习参考
需要运行该测试,需要下载单点登录代码,并且运行,运行教程需要参考
运行步骤:
1. 启动sso
2. 启动wolf-permission-management
测试
通过用户名密码异步请求获取token
http://localhost:8082/user/login?client_name=rest&username=admin&password=123
返回的json
{
"token": "eyJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiZGlyIn0..zDvUm-Q9YhwdvcR1.fz5-ar7FEWEnisDjyjZZ5lgb8xWaS5sffOafYUeZ_-sJJLSx2utoDOK2d1gS8G2oYbg7kbSR_Pcb_m3x3a_awLsoki79LOtk-yI1INWcYJTuF_zY27JPIqAOXF8GFwIc0QzuPke6mLoVUzuwc7ILjvqXg9Z2VA7hWrYJZ1WNsO3UDyPAltq9TXgzl3aJc3XBAUXPNzw8BLUDclTUGs1MnNzjlZYI94qVZgsybZwokkXh1WZ8JEnc7XGtFNJVQOHiIbhSCYJgkjb5xjEtZRRbI7LGSn-kPm99tau68hrR3qbcZofR3lxZ8p-Cta9EHiZ-SQGFg6yBNFSslQnNSoYPuyOo4kFtUgUvG9QJJgwMj8E_sXnixJ4rBkcLuok7mBvkelYr8CbBUyMdZJvwhPCEt9yaBDxJSGL-osWhvseoFFVc9Rp-Egie-NCuUzrygzi2juwMyLLsHybQL6m77rn3Hi_flgAtClUeyuwLbCSQ_Qn2fSz31NCydkxsC7NIsMf-VDiQwQMe9eO3WLcAkr1EAUCyMse61eww9n944350oXhMAQLpShr45AEXNHQ7wls5ZS1Wh6llm4kpQyMh_fwfNEAS7bPBlncKc9rjMloYaQPIkshxh5qysVxPlmWSctGxDdQMzQoJWGaeA9ZXSivF0p_zDAAqM0PAwUPYlBlRkCbO-2I7fapCs-5r0zpZBvYCwYxr4-h1n98x.v8ucroIDOmJbMLLwUkLUow"
}
通过token访问资源
http://localhost:8082/user/detail?client_name=jwt&token=eyJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiZGlyIn0..zDvUm-Q9YhwdvcR1.fz5-ar7FEWEnisDjyjZZ5lgb8xWaS5sffOafYUeZ_-sJJLSx2utoDOK2d1gS8G2oYbg7kbSR_Pcb_m3x3a_awLsoki79LOtk-yI1INWcYJTuF_zY27JPIqAOXF8GFwIc0QzuPke6mLoVUzuwc7ILjvqXg9Z2VA7hWrYJZ1WNsO3UDyPAltq9TXgzl3aJc3XBAUXPNzw8BLUDclTUGs1MnNzjlZYI94qVZgsybZwokkXh1WZ8JEnc7XGtFNJVQOHiIbhSCYJgkjb5xjEtZRRbI7LGSn-kPm99tau68hrR3qbcZofR3lxZ8p-Cta9EHiZ-SQGFg6yBNFSslQnNSoYPuyOo4kFtUgUvG9QJJgwMj8E_sXnixJ4rBkcLuok7mBvkelYr8CbBUyMdZJvwhPCEt9yaBDxJSGL-osWhvseoFFVc9Rp-Egie-NCuUzrygzi2juwMyLLsHybQL6m77rn3Hi_flgAtClUeyuwLbCSQ_Qn2fSz31NCydkxsC7NIsMf-VDiQwQMe9eO3WLcAkr1EAUCyMse61eww9n944350oXhMAQLpShr45AEXNHQ7wls5ZS1Wh6llm4kpQyMh_fwfNEAS7bPBlncKc9rjMloYaQPIkshxh5qysVxPlmWSctGxDdQMzQoJWGaeA9ZXSivF0p_zDAAqM0PAwUPYlBlRkCbO-2I7fapCs-5r0zpZBvYCwYxr4-h1n98x.v8ucroIDOmJbMLLwUkLUow
返回
users:admin
下章介绍
最近很多朋友问我,QQ登录、微信登录如何集成,怎么做?这些都是第三方的登录集成,目前通常的做法都是采用OAuth2协议来解决,cas统称为代理登录
其实这个问题也延伸出好几个问题:
- SSO集成OAuth Client
- 业务系统集成OAuth
- 第三方登录后,用户绑定(如csdn通过github登录,要求绑定csdn账号)
我觉得最有意思的是第三点,那么下一章给大家讲解一下,也简单写个demo,当然由于国内的网络安全法限制的多,将不采取QQ登录或者微信登录,直接采用GitHub的登录即可,因为都是OAuth2登录,但qq的登录是返回xml,这个是需要另解决的
作者联系方式
如果技术的交流或者疑问可以联系或者提出issue。
QQ: 756884434 (请注明:SSO-CSDN)
上一篇: 关于Maven如何打Zip包