SpringBoot+Shiro+CAS创建CAS客户端,在实际的应用中,是把这个创建的客户端做成一个通用的jar包,当有服务需要接入到CAS服务端时,就让这个服务引入这个客户端就可以了。
这个客户端是通过shiro做登录验证、以及权限验证,登录验证是验证一些如:用户是否被禁用,是否黑名单,是否被关小黑屋等等,账号和密码验证是在CAS服务端做的,这里是不做的。
这篇文章只是简单的整合了SpringBoot+Shiro+CAS。
注意:这些代码都是已经跑通过的,其中涉及到单点登录和单点登出,其中单点登录可以使用localhost,但是单点登出,就必须要域名,如果不知道怎么做本地域名,那么就看看这篇文章:Win10 如何把本地ip映射成域名_我是混IT圈的-CSDN博客
其中有两个问题没有解决:
1、客户端返回了登录错误,服务端要怎么展示?
2、服务端的登录,不只是账号和密码,还需要验证码登录,怎么改进服务端?
整体的项目结构
废话不多说,上代码
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文件,需要引用下。