SSO单点登录(三)SpringBoot+Shiro+CAS创建CAS客户端

SpringBoot+Shiro+CAS创建CAS客户端,在实际的应用中,是把这个创建的客户端做成一个通用的jar包,当有服务需要接入到CAS服务端时,就让这个服务引入这个客户端就可以了。

这个客户端是通过shiro做登录验证、以及权限验证,登录验证是验证一些如:用户是否被禁用,是否黑名单,是否被关小黑屋等等,账号和密码验证是在CAS服务端做的,这里是不做的。

这篇文章只是简单的整合了SpringBoot+Shiro+CAS。

注意:这些代码都是已经跑通过的,其中涉及到单点登录单点登出,其中单点登录可以使用localhost,但是单点登出,就必须要域名,如果不知道怎么做本地域名,那么就看看这篇文章:Win10 如何把本地ip映射成域名_我是混IT圈的-CSDN博客

其中有两个问题没有解决:

1、客户端返回了登录错误,服务端要怎么展示?

2、服务端的登录,不只是账号和密码,还需要验证码登录,怎么改进服务端?

整体的项目结构

SSO单点登录(三)SpringBoot+Shiro+CAS创建CAS客户端

废话不多说,上代码

pom.xml

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-all</artifactId>
            <version>1.8.0</version>
        </dependency>
        <!--shiro与cas整合-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-cas</artifactId>
            <version>1.8.0</version>
        </dependency>
        <!--shiro与spring整合-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.8.0</version>
        </dependency>
        <!--shiro与thymeleaf整合-->
        <dependency>
            <groupId>com.github.theborakompanioni</groupId>
            <artifactId>thymeleaf-extras-shiro</artifactId>
            <version>2.1.0</version>
        </dependency>

ShiroCasConfig

package com.example.casclient.config;

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import com.example.casclient.shiro.AdminRealm;
import org.apache.shiro.cas.CasFilter;
import org.apache.shiro.cas.CasSubjectFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.authc.LogoutFilter;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.jasig.cas.client.authentication.AuthenticationFilter;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.*;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroCasConfig {
    /**
     * 	配置权限管理器(核心)
     * @return
     */
    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //选择cas的SubjectFactory
        securityManager.setSubjectFactory(new CasSubjectFactory());
        //选择自定义的realm
        securityManager.setRealm(adminRealm());
        return securityManager;
    }

    /**
     * 	shiro过滤器
     * @return
     */
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter() {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        //Shiro的核心安全接口,这个属性是必须的
        shiroFilter.setSecurityManager(securityManager());

        //要求登录时的链接(可根据项目的URL进行替换),非必须的属性,默认会自动寻找Web工程根目录下的"/login.html"页面,结尾的shiro-cas必须的.
        shiroFilter.setLoginUrl("http://sso.cas.com:8080/cas/login?service=http://a.cas.com:8081/shiro-cas");

        //登录成功后要跳转的连接
        shiroFilter.setSuccessUrl("/index.html");

        // 用户访问未对其授权的资源时,所显示的连接
        //若想更明显的测试此属性可以修改它的值,如unauthor.jsp,然后用登录后访问/admin/listUser.jsp就看见浏览器会显示unauthor.jsp
        shiroFilter.setUnauthorizedUrl("/sso/unauthorized");

        /** Shiro连接约束配置,即过滤链的定义
         *	此处可配合我的这篇文章来理解各个过滤连的作用http://blog.csdn.net/jadyer/article/details/12172839 -->
         * 	下面value值的第一个'/'代表的路径是相对于HttpServletRequest.getContextPath()的值来的 -->
         * 	anon:它对应的过滤器里面是空的,什么都没做,这里.do和.jsp后面的*表示参数,比方说login.jsp?main这种 -->
         *	authc:该过滤器下的页面必须验证后才能访问,它是Shiro内置的一个拦截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter -->
         */
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("casFilter",casFilter());
        filterMap.put("logout",logoutFilter());
        shiroFilter.setFilters(filterMap);

        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();

        filterChainDefinitionMap.put("/shiro-cas", "casFilter");//cas登录
        filterChainDefinitionMap.put("/logout", "logout");//cas登出
        //配置本服务的其他需要拦截的url和不需要拦截的url,authc:需要登录  anon:不需要登录
        filterChainDefinitionMap.put("/*.js", "anon");//静态文件不需要验证
        filterChainDefinitionMap.put("/index.html", "anon");//首页
        filterChainDefinitionMap.put("/fail.html", "anon");//登录失败页面

        filterChainDefinitionMap.put("/**", "authc");

        shiroFilter.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilter;
    }

    //cas的登录过滤器
    public CasFilter casFilter(){
        CasFilter casFilter = new CasFilter();
        /**
         * 登录失败,先调用登出的接口,在跳转到错误页面
         * 因为这里的登录失败是因为客户端的用户状态验证,比如被禁用,黑名单,离职等原因而不能登录的,而不是服务端的账号密码错误导致不能登录的.
         * 那么对于服务端来说,客户端是登录成功的,所以如果客户端不调用服务端的退出接口,那么就会出现客户端一直无法重新进入登录页面。
         */
        casFilter.setFailureUrl("http://sso.cas.com:8080/cas/logout?service=http://a.cas.com:8081/fail.html");
        return casFilter;
    }

    //cas的登出过滤器
    public LogoutFilter logoutFilter(){
        LogoutFilter logoutFilter = new LogoutFilter();
        //退出重定向后,跳转到本系统的首页
        logoutFilter.setRedirectUrl("http://sso.cas.com:8080/cas/logout?service=http://a.cas.com:8081/index.html");
        return logoutFilter;
    }

    /**
     * 自定义 Realm
     * @return
     */
    @Bean
    public AdminRealm adminRealm(){
        AdminRealm adminRealm = new AdminRealm();
        //cas服务器地址
        adminRealm.setCasServerUrlPrefix("http://sso.cas.com:8080/cas");
        //当前服务的地址,必须要加上 shiro-cas,和上面 shiroFilter登录地址,filterChainDefinitionMap.put("/shiro-cas", "casFilter"),这里三个地方要一样
        adminRealm.setCasService("http://a.cas.com:8081/shiro-cas");
        return adminRealm;
    }

    /**
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }
    /**
     * 开启shiro aop注解支持.
     * 使用代理方式;所以需要开启代码支持;
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(org.apache.shiro.mgt.SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
    /**
     * 开启jsp/html页面的注解
     * @return
     */
    @Bean
    public ShiroDialect shiroDialect(){
        return new ShiroDialect();
    }

}

AdminRealm

package com.example.casclient.shiro;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.cas.CasRealm;
import org.apache.shiro.subject.PrincipalCollection;

import java.util.ArrayList;
import java.util.List;

public class AdminRealm extends CasRealm {

    /**
     * 设置角色和权限信息
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("=============5");
        String username = (String) principals.getPrimaryPrincipal();
        //根据用户名从数据库查询用户信息
        System.out.println("2根据用户名从数据库查询用户信息:"+username);
        if (username != null) {
            SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
            //查询获取用户拥有的角色

            //添加权限
            List<String> permissions = new ArrayList<>();
            permissions.add("user:list");
            permissions.add("user:update");
            authorizationInfo.addStringPermissions(permissions);
            return authorizationInfo;
        } else {
            return null;
        }
    }


    /**
     * CAS认证 ,验证用户身份
     * 将用户基本信息设置到会话中
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
        System.out.println("=============4");
        //调用CasRealm实现的认证方法,其包含验证ticket、填充CasToken的principal等操作)
        AuthenticationInfo authc = super.doGetAuthenticationInfo(token);
        Object primaryPrincipal = authc.getPrincipals().getPrimaryPrincipal();
        if (primaryPrincipal == null){
            return null;
        }
        //用户名
        String username = (String) primaryPrincipal;
        System.out.println("1根据用户名从数据库查询用户信息:"+username);
        if (username != null) {
            //将用户信息放在session
            SecurityUtils.getSubject().getSession().setAttribute("user", username);
            /*if (1 == 1){
                throw new AuthenticationException("msg:该帐号禁止登录");
            }*/
            return authc;
        }
        return null;
    }

    @Override
    protected void clearCachedAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("=============3");
        super.clearCachedAuthorizationInfo(principals);
    }

    @Override
    protected void clearCachedAuthenticationInfo(PrincipalCollection principals) {
        System.out.println("=============2");
        super.clearCachedAuthenticationInfo(principals);
    }

    @Override
    protected void clearCache(PrincipalCollection principals) {
        System.out.println("=============1");
        super.clearCache(principals);
    }
}

 HomeController

package com.example.casclient.controller;

import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Controller
public class HomeController {

    //首页,不需要权限
    @GetMapping("/index.html")
    public String home(){
        return "index";
    }

    //详情,需要登录
    @GetMapping("/details.html")
    public String details(){
        return "details";
    }

    //登录失败
    @GetMapping("/fail.html")
    public String fail(HttpServletRequest request){
        Object assertion = request.getSession().getAttribute("user");
        System.out.println("登录失败,失败原因:"+assertion);
        return "fail";
    }

    //获取用户信息
    @ResponseBody
    @RequiresPermissions(value = {"user:info","user:update"},logical = Logical.OR)//资源权限的注解
    @PostMapping("/userInfo")
    public List<Map<String,String>> userInfo(HttpServletRequest request, String userName){
        //从session中获取登录用户的信息,这个地方是在自定义的realm中把用户信息存在session中的.
        Object assertion = request.getSession().getAttribute("user");
        System.out.println("===========assertion:"+assertion);
        System.out.println("==========userName:"+userName);
        List<Map<String,String>> list = new ArrayList<>();
        Map<String,String> map = new HashMap<>();
        map.put("userName","小王");
        map.put("gender","男");
        map.put("age","25");
        list.add(map);
        Map<String,String> map1 = new HashMap<>();
        map1.put("userName","小红");
        map1.put("gender","女");
        map1.put("age","21");
        list.add(map1);
        return list;
    }

}

启动类:CasClientApplication

package com.example.casclient;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.util.ResourceUtils;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

@SpringBootApplication
public class CasClientApplication extends WebMvcConfigurationSupport {

    public static void main(String[] args) {
        SpringApplication.run(CasClientApplication.class, args);
    }

    // 配置静态资源文件路径
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**").addResourceLocations("classpath:/resources/")
                .addResourceLocations("classpath:/static/").addResourceLocations("classpath:/public/");
        super.addResourceHandlers(registry);
    }
}

index.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script type="text/javascript" src="/jquery.min.js"></script>
</head>
<body>
<h1>淘宝</h1>
    <span>这里是首页,不需要登录</span><br/><br/>
    <a href="http://a.cas.com:8081/details.html">详情</a>
</body>
</html>

details.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script type="text/javascript" src="/jquery.min.js"></script>
</head>
<body>
<h1>淘宝</h1>
    <span>这里是详情页面,需要登录</span><br/><br/><br/>

    <!--通过资源来控制权限-->
    <shiro:hasPermission name="user:list">
        <p>用户必须有[user:list]的权限才可以看见这里的内容</p><br/>
        <button id="_button">获取用户信息</button>
        <br/><br/>
        <div>
            <table>
                <tr>
                    <td>姓名</td>
                    <td>性别</td>
                    <td>年龄</td>
                </tr>
                <tbody id="_tbody"></tbody>
            </table>
        </div>
    </shiro:hasPermission>
    <br/><br/><br/><br/>
    <a href="/logout">退出</a><!--这个接口已经被cas的退出过滤器拦截了-->
</body>
<script>
    $("#_button").click(function (){
        $.ajax({
            type: "post",
            url:"/userInfo",
            data: {userName:"呵呵呵"},
            success: function (res) {
                alert(JSON.stringify(res));
                var _html = "<tr>";
                for (i=0;i<res.length;i++){
                    _html += "<td>"+res[i].userName+"</td>";
                    _html += "<td>"+res[i].gender+"</td>";
                    _html += "<td>"+res[i].age+"</td>";
                    _html += "</tr>";
                }
                $("#_tbody").html(_html);
            }
        });
    });
</script>
</html>

fail.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>淘宝</h1>
  <span>登录失败</span><br/><br/><br/>
    <a href="http://sso.cas.com:8080/cas/login?service=http://a.cas.com:8081/shiro-cas">重新登录</a><br/><br/>
</body>
</html>

配置文件就是一个端口号

server.port=8081

html文件中,有个jquery的js文件,需要引用下。

上一篇:zabbix监控流程


下一篇:Spring Cloud + Spring Boot + Mybatis + Uniapp 企业架构之CAS SSO单点登录服务端环境搭建