欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

CAS单点登录-客户端集成(shiro、springboot、jwt、pac4j)(十)

程序员文章站 2024-03-20 20:17:58
...

CAS单点登录-客户端集成(shiro springboot jwt pac4j)(十)

由于我们通常在业务上会有以下的使用场景:

  • 移动端通过业务系统鉴权
  • 移动端免登录(登录一次以后)

解决方案:

  • JWT(token认证方案)
  • OAuth(第三方认证)

PS:若想继续往下读,必须具备JWT的基本概念以及Pac4j的认证原理及应用场景


疑问

当然我们这章是讲JWT,那么会有以下的疑问:

  1. 若服务端已经接入了SSO,那么在移动端用户登录信息提交给SSO还是服务端?(毫无疑问是服务端,SSO对于移动端必须是透明的)
  2. 若采用无会话方式,如何获取token,服务端如何鉴权?(1.提交用户名密码到服务端,服务端把数据给到sso,sso最终返回用户数据 2. 根据用户数据创建token返回给移动端 3. 移动端登录后请求都带token给到服务端鉴权)
  3. 在使用token鉴权的情况下,退出如何解决?(客户端丢弃token即可)

鉴权流程

我们再讲一下整一个鉴权流程

Created with Raphaël 2.1.0MobileMobileServerServerSSOSSO提交用户名密码提交用户名密码鉴权(pac4j)成功返回UserProfile根据profile创建token返回jwt token携带token请求资源Shiro Subject.login()鉴权通过返回报文

配置要素

重点: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);
}

由于以上代码仅做为学习参考
需要运行该测试,需要下载单点登录代码,并且运行,运行教程需要参考

下载代码尝试:CAS单点登录-客户端集成(shiro、springboot、jwt、pac4j)(十) CAS单点登录-客户端集成(shiro、springboot、jwt、pac4j)(十)

运行步骤:
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。

邮箱:[email protected]

QQ: 756884434 (请注明:SSO-CSDN)

上一篇: 关于Maven如何打Zip包

下一篇: