Spring Boot+Spring Security+CAS实现单点登录

配套资料,免费下载
链接:https://pan.baidu.com/s/1EINPwP4or0Nuj8BOEPsIyw
提取码:kbue
复制这段内容后打开百度网盘手机App,操作更方便哦

第一章 CAS的概述

1.1、SSO

单点登录(SingleSignOn,SSO),就是通过用户的一次性鉴别登录。当用户在身份认证服务器上登录一次以后,即可获得访问单点登录系统中其他关联系统和应用软件的权限,同时这种实现是不需要管理员对用户的登录状态或其他信息进行修改的,这意味着在多个应用系统中,用户只需一次登录就可以访问所有相互信任的应用系统。这种方式减少了由登录产生的时间消耗,辅助了用户管理,是目前比较流行的一种分布式登录方式。

1.2、CAS

CAS(Central Authentication Service的缩写,中央认证服务)是耶鲁大学 Technology and Planning 实验室的 Shawn Bayern 在2002年出的一个开源系统。刚开始名字叫Yale CAS。Yale CAS 1.0的目标只是一个单点登录的系统,随着慢慢公开,功能就越来越多了,2.0就提供了多种认证的方式。从结构上看,CAS 包含两个部分: CAS Server 和 CAS Client。

2004年12月,CAS转成 JASIG(Java Administration Special Interesting Group) 的一个项目,项目也随着改名为 JASIG CAS,这就是为什么现在有些CAS的链接还是有 jasig 的字样。

2012年,JASIG 跟 Sakai 基金会合并,改名为 Apereo 基金会,所有 CAS 也随着改名为 Apereo CAS。

  • 官网地址:https://www.apereo.org/projects/cas

  • 源码地址:https://github.com/apereo/cas-overlay-template/tree/5.3

CAS 具有以下特点:

  1. 开源的企业级单点登录解决方案。
  2. CAS Server 为需要独立部署的 Web 应用。
  3. CAS Client 支持非常多的客户端(这里指单点登录系统中的各个 Web 应用),包 括 Java,.Net,PHP,Perl,Apache,uPortal,Ruby 等。

第二章 CAS的流程

2.1、CAS服务端

CAS Server 需要独立部署,主要负责对用户的认证工作;

2.2、CAS客户端

CAS Client 负责处理对客户端受保护资源的访问请求,需要登录时,重定向到 CAS Server。

2.3、CAS流程图

下图是 CAS 最基本的协议过程,主要有以下步骤:

  1. 访问服务: CAS 客户端发送请求访问应用系统提供的服务资源。
  2. 定向认证: CAS 客户端会重定向用户请求到 CAS 服务器。
  3. 用户认证:用户身份认证。
  4. 发放票据: CAS 服务器会产生一个随机的 Service Ticket 。
  5. 验证票据: CAS 服务器验证票据 Service Ticket 的合法性,验证通过后,允许客户端访问服务。
  6. 传输用户信息:CAS 服务器验证票据通过后,传输用户认证结果信息给客户端。

第三章 CAS的部署

3.1、源码下载

下载地址:https://codeload.github.com/apereo/cas-overlay-template/zip/5.3

注意:由于github是在国外,可能个别人下载会很慢,你可以使用手机下载试试,并且尝试一下不同的网络。

3.2、源码打包

打包命令:mvn clean package -Dmaven.test.skip=true

注意:打包的速度是很快的,但是打包所需要的依赖均在互联网上,个别包非常大,第一次打包会下载很多东西很慢,请耐心等候,如果失败请多尝试几次。

image-20210123130755874

image-20210123135359761

更换下载源

如果你想要下载速度快点,请更换下载源,找到pom.xml中的repositories,把以下代码放到第一个,具体如下图:

<repository>
    <id>aliyunmaven</id>
    <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
</repository>

image-20210123151202433

3.3、部署运行

由于打完包后是一个war包,需要tomcat进行部署,然后才能运行,如果你没有tomcat请下载。

  • 32位tomcat:https://mirrors.bfsu.edu.cn/apache/tomcat/tomcat-8/v8.5.61/bin/apache-tomcat-8.5.61-windows-x86.zip
  • 64位tomcat:https://mirrors.bfsu.edu.cn/apache/tomcat/tomcat-8/v8.5.61/bin/apache-tomcat-8.5.61-windows-x64.zip

将打包好的war包,放到tomcatwebapps目录中,然后点击bin目录下的startup.bat启动。

注意:如果你在启动的过程中,发现tomcat一闪而过或启动失败,可能的原因就是jdk的环境变量没有配好,在一个就是端口占用了,tomcat默认端口为8080。

如果一切运行正常,那么你在浏览器中输入地址:http://localhost:8080/cas/login,将会看到如下界面:

  • 默认用户名:casuser

  • 默认密码:Mellon

image-20210123135831324

image-20210123140056569

除了使用上边提供的登出链接,还可以手动在地址栏输入http://localhost:8080/cas/logout

image-20210123140144713

第四章 CAS的定制

4.1、定制数据源

(1)、加入数据源依赖包

找到源码目录cas-overlay-template-5.3中的pom.xml,在第125-127注释后边加入如下依赖代码:

<!--数据库认证相关 start-->
<dependency>
    <groupId>org.apereo.cas</groupId>
    <artifactId>cas-server-support-jdbc</artifactId>
    <version>${cas.version}</version>
</dependency>
<dependency>
    <groupId>org.apereo.cas</groupId>
    <artifactId>cas-server-support-jdbc-drivers</artifactId>
    <version>${cas.version}</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.49</version>
</dependency>
<!--数据库认证相关 end-->

image-20210123141637428

(2)、重新打包部署项目

重新打包

找到源码目录cas-overlay-template-5.3,在地址栏输入cmd,然后回车,运行打包命令:mvn clean package -Dmaven.test.skip=true,这一次打包速度会很快,因为大部分依赖都已经下载到了本地,但是因为又新增了部分数据源的依赖,所以还是会联网下载,请耐心等待,我发誓这是最后一次打包了。

关闭项目

把之前开启的tomcat关掉。

删除文件

找到apache-tomcat-8.5.61\webapps目录,把里边所有的文件及目录全部删除干净(包括tomcat自带的),因为都没有用,留着反而会影响启动速度。

image-20210123141105031

重新部署

把打包好的war包,重新放到webapps目录中,然后启动tomcat服务器进行解压。

(3)、加入数据源的配置

打开apache-tomcat-8.5.61\webapps\cas\WEB-INF\classes解压后的类路径目录,在这里你将会看到很多配置文件,接下来,我们需要加上数据库的相关配置,具体代码如下,把他复制到application.properties的最后部分,将 cas.authn.accept.users=.... 加井号注释掉,再加入以下内容:

cas.authn.jdbc.query[0].driverClass=com.mysql.jdbc.Driver
cas.authn.jdbc.query[0].url=jdbc:mysql://localhost:3306/test
cas.authn.jdbc.query[0].user=root
cas.authn.jdbc.query[0].password=root
cas.authn.jdbc.query[0].sql=select password from sys_user where username=?
cas.authn.jdbc.query[0].fieldPassword=password

image-20210123152938977

(4)、加入密码加密规则

cas5.X 提供了4种加密配置:

cas.authn.jdbc.query[0].passwordEncoder.type=NONE|DEFAULT|STANDARD|BCRYPT

默认值为 NONE这四种方式其实脱胎于spring security中的加密方式,spring security提供了 MD5PasswordEncoder、SHAPasswordEncoder、StandardPasswordEncoder和 BCryptPasswordEncoder。

  • NONE:说明对密码不做任何加密,也就是保留明文。

  • DEFAULT:启用DefaultPasswordEncoder。 MD5PasswordEncoder和 SHAPasswordEncoder加密是编码算法加密,现在cas把他们归属于 DefaultPasswordEncoder。DefaultPasswordEncoder需要带参数 encodingAlgorithm:

    cas.authn.accept.passwordEncoder.encodingAlgorithm=MD5|SHA
    
  • STANDARD:启用StandardPasswordEncoder加密方式 。1024次迭代的 SHA‐256 散列 哈希加密实现,并使用一个随机8字节的salt。

  • BCRYPT:启用BCryptPasswordEncoder加密方式。

我们这里采用BCRYPT加密,因为我们数据库中的密码加密方式就是这种加密,应该保持一致,修改application.properties 配置文件。

cas.authn.jdbc.query[0].passwordEncoder.type=BCRYPT
cas.authn.jdbc.query[0].passwordEncoder.characterEncoding=UTF-8

image-20210123154113105

(5)、导入数据库数据表

DROP DATABASE IF EXISTS `test`;

CREATE DATABASE `test`;

USE `test`;

DROP TABLE IF EXISTS `sys_role`;

CREATE TABLE `sys_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '角色编号',
  `name` varchar(32) NOT NULL COMMENT '角色名称',
  `desc` varchar(32) NOT NULL COMMENT '角色描述',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

insert  into `sys_role`(`id`,`name`,`desc`) values (1,'ROLE_USER','用户权限');
insert  into `sys_role`(`id`,`desc`) values (2,'ROLE_ADMIN','管理权限');
insert  into `sys_role`(`id`,`desc`) values (3,'ROLE_PRODUCT','产品权限');
insert  into `sys_role`(`id`,`desc`) values (4,'ROLE_ORDER','订单权限');

DROP TABLE IF EXISTS `sys_user`;

CREATE TABLE `sys_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户编号',
  `username` varchar(32) NOT NULL COMMENT '用户名称',
  `password` varchar(128) NOT NULL COMMENT '用户密码',
  `status` int(1) NOT NULL DEFAULT '1' COMMENT '用户状态(0:关闭、1:开启)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

insert  into `sys_user`(`id`,`username`,`password`,`status`) values (1,'zhangsan','$2a$10$M7fmKpMZEkkzrTBiKie.EeAKZhQDrWAltpCA1y/py5AU/8lyiNB8y',0);
insert  into `sys_user`(`id`,`status`) values (2,'lisi',`status`) values (3,'wangwu',0);

DROP TABLE IF EXISTS `sys_user_role`;

CREATE TABLE `sys_user_role` (
  `uid` int(11) NOT NULL COMMENT '用户编号',
  `rid` int(11) NOT NULL COMMENT '角色编号',
  PRIMARY KEY (`uid`,`rid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert  into `sys_user_role`(`uid`,`rid`) values (1,1);
insert  into `sys_user_role`(`uid`,3);
insert  into `sys_user_role`(`uid`,`rid`) values (2,4);
insert  into `sys_user_role`(`uid`,`rid`) values (3,2);
insert  into `sys_user_role`(`uid`,4);

(6)、重新启动tomcat

把之前启动的tomcat关闭,然后重新启动,让咱们的配置生效。

(7)、重新开浏览器测试

打开浏览器输入地址:http://localhost:8080/cas/login

  • 账户:zhangsan
  • 密码:全部都是123456

4.2、兼容 HTTP

(1)、由于CAS默认使用的是基于https协议,需要改为兼容使用http协议,到apache-tomcat-8.5.61\webapps\cas\WEB-INF\classes目录的application.properties添加如下的内容,其中,TGC:Ticket Granted Cookie (客户端用户持有,传送到服务器,用于验证)存放用户身份认证凭证的cookie,在浏览器和CAS Server间通讯时使用,并且只能基于安全通道传输(Https),是CAS Server用来明确用户身份的凭证。

cas.tgc.secure=false
cas.serviceRegistry.initFromJson=true

(2)、到apache-tomcat-8.5.61\webapps\cas\WEB-INF\classes\services目录下的 HTTPSandIMAPS-10000001.json 修改内容如下,即添加http

{
  "@class" : "org.apereo.cas.services.RegexRegisteredService","serviceId" : "^(https|http|imaps)://.*","name" : "HTTPS and IMAPS","id" : 10000001,"description" : "This service definition authorizes all application urls that support HTTPS and IMAPS protocols.","evaluationOrder" : 10000
}

(3)、重新启动tomcat,然后登录zhangsan进行测试,或许看起来并没有什么不一样,但是现在已经兼容http协议了。

4.3、定制登录页

相关规范

● 静态资源(js、css)存放目录为WEB-INF\classes\static
● html资源(thymeleaf模板)存放目录为WEB-INF\classes\templates
● 主题配置文件存放在WEB-INF\classes,并且命名为[theme_name].properties

准备工作

注意:配套资料中有静态资源,请在里边找到cas单点登录基础代码\登录页面源代码

(1)、在静态资源目录(WEB-INF\classes\static\themes)下创建一个文件夹,一般跟工程名字保持一致,这里我们叫mylogin,然后把咱们的css和js都拷贝进去。

image-20210123182404207

(2)、在模板资源目录(WEB-INF\classes\static\templates)下创建一个文件夹,一般跟工程名字保持一致,这里我们叫mylogin,把咱们的静态资源 login.html 拷贝到这里,然后把名称改为 casLoginView.html

image-20210123182608043

(3)、给登录页面模板添加thymeleaf的命名空间

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

(4)、修改所有静态资源的路径,改成绝对路径

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>自定义登录页</title>
    <link rel="stylesheet" href="/cas/themes/mylogin/css/bootstrap.min.css">
</head>
<body>
...
...    
<script src="/cas/themes/mylogin/js/jquery-3.5.1.min.js"></script>
<script src="/cas/themes/mylogin/js/bootstrap.bundle.min.js"></script>
</body>
</html>

(5)、修改表单的提交路径并加上特定对象属性

<form th:object="${credential}" action="login" method="post">
...
...    
</form>

(6)、修改表单的用户名和密码文本框都添加上一个th:field属性,并去掉无用属性

...
<input type="text" class="form-control" th:field="*{username}" placeholder="请输入用户" required>
...
<input type="text" class="form-control" th:field="*{password}" placeholder="请输入密码" required>
...

(7)、为此表单添加登录失败错误信息以及隐藏域表单项,直接拷贝以下代码到button提交按钮上边即可

<div class="form-group" th:if="${#fields.hasErrors('*')}">
    <span th:each="err : ${#fields.errors('*')}" th:utext="${err}"></span>
</div>
<input type="hidden" name="execution" th:value="${flowExecutionKey}"/>
<input type="hidden" name="_eventId" value="submit"/>
<input type="hidden" name="geolocation"/>

(8)、在apache-tomcat-8.5.61\webapps\cas\WEB-INF\classes目录下创建一个主题配置文件,名字叫mylogin.properties,配置内容如下:

cas.standard.css.file=/css/cas.css

(9)、找到application.properties配置文件,在文件的最后,我们启用默认主题为我们自己的主题即可。

cas.theme.defaultThemeName=mylogin

(10)、重启tomcat应用,访问登录地址:http://localhost:8080/cas/login,如果一切顺利,那么,您将会看到如下界面:

image-20210123185257044

(11)、登录测试,使用账号zhangsan,密码:123456进行登录测试。好就这样,我们保持工程启动状态,接下来,进行下一环节。

image-20210123185401577

第五章 CAS的集成

5.1、工程创建

image-20210123190235318

image-20210123190258544

image-20210123190317675

image-20210123190352087

5.2、导入依赖

pom.xml

<!--跟数据库进行的整合-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.4</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.49</version>
</dependency>
<!--跟Spring Security+CAS进行的整合-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-cas</artifactId>
</dependency>

5.3、修改包名

com.caochenlei.casresourceorder修改为com.caochenlei,然后在分别创建以下包名

  1. com.caochenlei.domain:用于存放实体对象
  2. com.caochenlei.prop:用于存放配置属性类
  3. com.caochenlei.mapper:用于存放映射文件
  4. com.caochenlei.service:用于存放服务接口
  5. com.caochenlei.controller:用于存放控制器类
  6. com.caochenlei.config:用于存放配置对象

将资料中提供的 cas单点登录基础代码\映射服务的代码 中的mapper和service中的代码直接拷贝到工程所对应的包中,也可以直接带包拷贝

5.4、编写配置文件

application.yaml

server:
  port: 9001
  servlet:
    application-display-name: cas-resource-order
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test
    username: root
    password: root
mybatis:
  type-aliases-package: com.caochenlei.domain
  configuration:
    map-underscore-to-camel-case: true
logging:
  level:
    com.caochenlei: debug

#自定义配置:APP配置信息(资源端)
app:
  server:
    host:
      #APP服务地址(绝对路径)
      url: http://localhost:9001
      #APP登录地址(相对路径)
      login_url: /login
      #APP退出地址(相对路径)
      logout_url: /logout

#自定义配置:CAS配置信息(认证端)
cas:
  server:
    host:
      #CAS服务地址(绝对路径)
      url: http://localhost:8080/cas
      #CAS登录地址(绝对路径)
      login_url: ${cas.server.host.url}/login
      #CAS退出地址(绝对路径)
      logout_url: ${cas.server.host.url}/logout?service=${app.server.host.url}

5.5、编写角色授权

com.caochenlei.service.CustomUserDetailsService

@Service
@Transactional
public class CustomUserDetailsService implements UserDetailsService {
    @Autowired(required = false)
    private SysUserMapper sysUserMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return sysUserMapper.findByUsername(username);
    }
}

5.6、编写配置对象

com.caochenlei.prop.CasProperties

/**
 * 自定义属性配置类
 */
@Component
@Data
public class CasProperties {
    @Value("${cas.server.host.url}")
    private String casServerUrl;

    @Value("${cas.server.host.login_url}")
    private String casServerLoginUrl;

    @Value("${cas.server.host.logout_url}")
    private String casServerLogoutUrl;

    @Value("${app.server.host.url}")
    private String appServerUrl;

    @Value("${app.server.host.login_url}")
    private String appServerLoginUrl;

    @Value("${app.server.host.logout_url}")
    private String appServerLogoutUrl;
}

com.caochenlei.config.SecurityConfig

@Configuration
@EnableWebSecurity //启用web权限
@EnableGlobalMethodSecurity(securedEnabled = true) //启用方法验证
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private CasProperties casProperties;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        super.configure(auth);
        auth.authenticationProvider(casAuthenticationProvider());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //禁用csrf保护机制
        http.csrf().disable();
        //禁用cors保护机制
        http.cors().disable();
        //禁用form表单登录
        http.formLogin().disable();
        //增加自定义过滤器
        http.exceptionHandling()
                .authenticationEntryPoint(casAuthenticationEntryPoint())
                .and()
                .addFilterAt(casAuthenticationFilter(), CasAuthenticationFilter.class)
                .addFilterBefore(logoutFilter(), LogoutFilter.class)
                .addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class);
    }

    /**
     * CAS认证入口点开始 =============================================================================
     */
    @Bean
    public CasAuthenticationEntryPoint casAuthenticationEntryPoint() {
        CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint();
        casAuthenticationEntryPoint.setLoginUrl(casProperties.getCasServerLoginUrl());
        casAuthenticationEntryPoint.setServiceProperties(serviceProperties());
        return casAuthenticationEntryPoint;
    }

    @Bean
    public ServiceProperties serviceProperties() {
        ServiceProperties serviceProperties = new ServiceProperties();
        serviceProperties.setService(casProperties.getAppServerUrl() + casProperties.getAppServerLoginUrl());
        serviceProperties.setAuthenticateAllArtifacts(true);
        return serviceProperties;
    }
    /**
     * CAS认证入口点结束 =============================================================================
     */

    /**
     * CAS认证过滤器开始 =============================================================================
     */
    @Bean
    public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
        CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
        casAuthenticationFilter.setAuthenticationManager(authenticationManager());
        casAuthenticationFilter.setFilterProcessesUrl(casProperties.getAppServerLoginUrl());
        return casAuthenticationFilter;
    }

    @Bean
    public CasAuthenticationProvider casAuthenticationProvider() {
        CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
        casAuthenticationProvider.setAuthenticationUserDetailsService(userDetailsByNameServiceWrapper());
        casAuthenticationProvider.setServiceProperties(serviceProperties());
        casAuthenticationProvider.setTicketValidator(new Cas20ServiceTicketValidator(casProperties.getCasServerUrl()));
        casAuthenticationProvider.setKey("an_id_for_this_auth_provider_only");
        return casAuthenticationProvider;
    }

    @Bean
    public UserDetailsByNameServiceWrapper userDetailsByNameServiceWrapper() {
        UserDetailsByNameServiceWrapper userDetailsByNameServiceWrapper = new UserDetailsByNameServiceWrapper();
        userDetailsByNameServiceWrapper.setUserDetailsService(userDetailsService());
        return userDetailsByNameServiceWrapper;
    }

    @Bean
    public UserDetailsService userDetailsService() {
        return new CustomUserDetailsService();
    }
    /**
     * CAS认证过滤器结束 =============================================================================
     */

    /**
     * CAS登出过滤器开始 =============================================================================
     */
    @Bean
    public LogoutFilter logoutFilter() {
        LogoutFilter logoutFilter = new LogoutFilter(casProperties.getCasServerLogoutUrl(), new SecurityContextLogoutHandler());
        logoutFilter.setFilterProcessesUrl(casProperties.getAppServerLogoutUrl());
        return logoutFilter;
    }

    @Bean
    public SingleSignOutFilter singleSignOutFilter() {
        SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
        singleSignOutFilter.setIgnoreInitConfiguration(true);
        return singleSignOutFilter;
    }
    /**
     * CAS登出过滤器结束 =============================================================================
     */
}

5.7、编写控制器类

com.caochenlei.controller.OrderController

@RestController
@RequestMapping("/order")
public class OrderController {
    @Secured({"ROLE_ADMIN", "ROLE_ORDER"})
    @RequestMapping("/info")
    public String info() {
        return "Order Controller ...";
    }
}

5.8、启动项目测试

启动项目,在浏览器输入地址进行访问:http://localhost:9001/order/info ,发现报错了,报错的原因很简单,zhangsan不能访问订单,只有lisi可以。

image-20210123211430575

那我们使用退出链接 http://localhost:9001/logout 进行退出。

image-20210123204732465

然后使用lisi进行重新访问:http://localhost:9001/order/info。

image-20210123204756232

登录以后,我们就可以看到我们的业务逻辑执行后所返回的数据了。

image-20210123213557859

相关文章

Nacos 中的参数有很多,如:命名空间、分组名、服务名、保护...
Nacos 支持两种 HTTP 服务请求,一个是 REST Template,另一...
Nacos 是 Spring Cloud Alibaba 中一个重要的组成部分,它提...
Spring Cloud Alibaba 是阿里巴巴提供的一站式微服务开发解决...
在 Nacos 的路由策略中有 3 个比较重要的内容:权重、保护阈...
前两天遇到了一个问题,Nacos 中的永久服务删除不了,折腾了...