在设计开发自己的博客系统时,选择cas+shiro做用户认证和鉴权的框架,实现单点登录,方便接入其他系统。
曾经在徐州客户现场被cas折磨好久,一直想把cas啃下来。原谅我是个小菜鸟
搭建这个框架爬了很多坑,也参考了很多大佬的博客,自己慢慢试,跟踪源码,慢慢思考,也有一点收获。
目前项目还没有完成,可能还有很多坑,有很多地方处理的不是很好,现在只是流程通了,先记录下来,省着时间长忘了思路。也请各位大佬帮忙之处不对的地方
cas服务器
https://github.com/jsong93/cas-server
客户端
https://github.com/jsong93/shiro-client
cas服务端搭建
cas md5+盐加密
spring boot cas+shiro环境搭建
shiroconfig配置
package com.jsong.wiki.backend.config;
import com.jsong.wiki.backend.bean.ShiroUrl;
import com.jsong.wiki.backend.filter.MyAuthenticationFilter;
import com.jsong.wiki.backend.filter.MyFormAuthenticationFilter;
import com.jsong.wiki.backend.filter.MyUserFilter;
import com.jsong.wiki.backend.shiro.ShiroCasRealm;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.cas.CasFilter;
import org.apache.shiro.cas.CasSubjectFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
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.filter.authc.UserFilter;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
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配置
*
* @Author: Jsong
* @Date: 2020/2/7 21:58
* @Description:
*/
@Configuration
public class ShiroConfig {
// shiroFilter
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager,
@Qualifier("casFilter") MyAuthenticationFilter casFilter,
// @Qualifier("logoutFilter") LogoutFilter logoutFilter,
@Qualifier("userFilter") MyUserFilter userFilter,
MyFormAuthenticationFilter myFormAuthenticationFilter,
ShiroUrl shiroUrl) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 设置登录URL,当用户未认证,但访问了需要认证后才能访问的页面,就会自动重定向到登录URL
shiroFilterFactoryBean.setLoginUrl(shiroUrl.getLoginUrl());
shiroFilterFactoryBean.setSuccessUrl(shiroUrl.getSuccessUrl());
// 设置没有权限的URL,当用户认证后,访问一个页面却没有权限时,就会自动重定向到没有权限的URL,若用户未认证访问一个需要权限的URL时,会跳转到登录URL
shiroFilterFactoryBean.setUnauthorizedUrl(shiroUrl.getUnauthorizedUrl());
Map<String, Filter> filters = new HashMap<>();
// org.apache.shiro.web.filter.mgt.DefaultFilter 包含所有的过滤器
filters.put("casFilter", casFilter);
filters.put("userFilter", userFilter);
filters.put("authc", myFormAuthenticationFilter);
// filters.put("logoutFilter", logoutFilter);
// 将Filter添加到Shiro过滤器链中,用于对资源设置权限
shiroFilterFactoryBean.setFilters(filters);
Map<String, String> filterChainDefinitionMap = new HashMap<String, String>();
filterChainDefinitionMap.put(shiroUrl.getCasFilterUrlPattern(), "casFilter");
// filterChainDefinitionMap.put(shiroUrl.getCasFilterUrlPattern(), "userFilter");
// filterChainDefinitionMap.put(shiroUrl.getLogoutUrlPattern(), "logoutFilter");
filterChainDefinitionMap.putAll(shiroUrl.getAuthUrlPatternMap());
// 配置哪些请求需要受保护,以及访问这些页面需要的权限
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
// 认证filter
@Bean
public MyAuthenticationFilter casFilter(ShiroUrl shiroUrl) {
MyAuthenticationFilter casFilter = new MyAuthenticationFilter();
// 登录成功url
casFilter.setSuccessUrl(shiroUrl.getSuccessUrl());
// 登录失败url
casFilter.setFailureUrl(shiroUrl.getFailureUrl());
return casFilter;
}
/***
* 配置登录后重定向filter
* @date 2020/2/29 20:41
* @author Jsong
* @param
* @return com.jsong.wiki.backend.filter.MyUserFilter
*/
@Bean
public MyUserFilter userFilter(){
return new MyUserFilter();
}
@Bean
public MyFormAuthenticationFilter myFormAuthenticationFilter(){
return new MyFormAuthenticationFilter();
}
// 自定义 casRealm
@Bean
public ShiroCasRealm casRealm(ShiroUrl shiroUrl, EhCacheManager ehCacheManager
) {
ShiroCasRealm casRealm = new ShiroCasRealm();
// cas服务器
casRealm.setCasServerUrlPrefix(shiroUrl.getCas().getServerUrlPrefix());
// 客户端地址,用于接收tiket
casRealm.setCasService(shiroUrl.getCas().getService());
casRealm.setCacheManager(ehCacheManager);
return casRealm;
}
/**
* 问题?????
* 把这个bean注入后,登录后就会重定向到登出,为什么呢?????
*
* @param shiroUrl
* @return
*/
// @Bean
public LogoutFilter logoutFilter(ShiroUrl shiroUrl) {
LogoutFilter logoutFilter = new LogoutFilter();
// 登出后重定向地址
logoutFilter.setRedirectUrl(shiroUrl.getLogoutUrl());
return logoutFilter;
}
@Bean
public EhCacheManager ehCacheManager() {
EhCacheManager ehCacheManager = new EhCacheManager();
ehCacheManager.setCacheManagerConfigFile("classpath:config/ehcache-shiro.xml");
return ehCacheManager;
}
// 配置securityManager SecurityManager,Shiro的安全管理,主要是身份认证的管理,缓存管理,cookie管理
@Bean
public SecurityManager securityManager(ShiroCasRealm casRealm, EhCacheManager ehCacheManager
) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setSubjectFactory(new CasSubjectFactory());
securityManager.setCacheManager(ehCacheManager);
securityManager.setRealm(casRealm);
return securityManager;
}
// 配置lifecycleBeanPostProcessor,shiro bean的生命周期管理器,可以自动调用Spring IOC容器中shiro bean的生命周期方法(初始化/销毁)
// @Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/* 为了支持Shiro的注解需要定义DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor两个bean
配置DefaultAdvisorAutoProxyCreator,必须配置了lifecycleBeanPostProcessor才能使用*/
// @DependsOn("lifecycleBeanPostProcessor")
// @Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
return defaultAdvisorAutoProxyCreator;
}
// 配置AuthorizationAttributeSourceAdvisor
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
重写CasFilter onAccessDenied方法
解决的问题
请求重定向返回cas登录页面的字符串
前端重定向,开启了一个新的session,shiro无法通过上次请求保存的saverequest中获取请求的uri,以至登录成功后,无法返回请求的页面
非前后端分离的项目,当请求一个需要权限的方法时,需要servlet可以直接重定向到cas登录页面
前后端分离的项目,当使用ajax请求一个需要权限的方法时,也会重定向,但会直接将重定向的cas登录页面以字符串的方式返回
解决办法
重写FormAuthenticationFilter的onAccessDenied方法,通过subject.isAuthenticated()判断是否登录,如果未登录返回给前端401,前端在重定向到cas登录页面
package com.jsong.wiki.backend.filter;
import com.jsong.wiki.backend.bean.CookieProperties;
import lombok.extern.log4j.Log4j2;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* authc 的过滤器
*
* @Author: Jsong
* @Date: 2020/3/2 22:24
* @Description:
*/
//@Component
@Log4j2
public class MyFormAuthenticationFilter extends FormAuthenticationFilter {
@Value("${shiro.loginUrl}")
private String loginUrl;
@Value("${front.url}")
private String frontUrl;
// protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
// log.info("-----------------redirect login --------------------");
// Cookie cookie = new Cookie("front-url", ((HttpServletRequest) request).getHeader("front-url"));
// ((HttpServletResponse) response).addCookie(cookie);
// saveRequest(request);
// redirectToLogin(request, response);
// }
@Autowired
private CookieProperties cookieProperties;
/***
*重写登录重定向
* 把请求内容存入cookie,重定向的时候带着cookie保存状态
* @date 2020/3/3 14:46
* @author Jsong
* @param request
* @param response
* @return boolean
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
if (isLoginRequest(request, response)) {
if (isLoginSubmission(request, response)) {
if (log.isTraceEnabled()) {
log.trace("Login submission detected. Attempting to execute login.");
}
return executeLogin(request, response);
} else {
if (log.isTraceEnabled()) {
log.trace("Login page view.");
}
//allow them to see the login page ;)
return true;
}
} else {
if (log.isTraceEnabled()) {
log.trace("Attempting to access a path which requires authentication. Forwarding to the " +
"Authentication url [" + getLoginUrl() + "]");
}
log.info("-----------------redirect login --------------------");
// Cookie[] cookies = ((HttpServletRequest) request).getCookies();
// for (Cookie cookie : cookies) {
// cookie.setDomain(cookieProperties.getDomain());
// cookie.setPath(cookieProperties.getPath());
// cookie.setHttpOnly(cookieProperties.getHttpOnly());
// }
// Cookie cookie = new Cookie(frontUrl, ((HttpServletRequest) request).getHeader(frontUrl));
// cookie.setDomain(cookieProperties.getDomain());
// cookie.setPath(cookieProperties.getPath());
// cookie.setHttpOnly(cookieProperties.getHttpOnly());
// ((HttpServletResponse) response).addCookie(cookie);
// saveRequestAndRedirectToLogin(request, response);
saveRequest(request);
// redirectToLogin(request, response);
// 读取到的 /index.jsp 不知道为啥
// String loginUrl = getLoginUrl();
// shiro 会把请求内容保存起来,未登录为空
// if(WebUtils.getSavedRequest(request)==null){
// // 未授权,返回前端状态401 进行重定向
// request里获取不到用户信息,不能通过request判断是否登录,不能用这个方法了
// ((HttpServletResponse) response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
// return false;
// }
Subject subject = SecurityUtils.getSubject();
if (!subject.isAuthenticated()) {
// 未授权,返回前端状态401 进行重定向
((HttpServletResponse) response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
// ajax 重定向会返回html页面字符串
WebUtils.issueRedirect(request, response, loginUrl);
return false;
}
}
}
/*
* @Descripttion:
* @version:
* @Author: jsong
* @Date: 2020-02-25 22:30:54
* @LastEditors: jsong
* @LastEditTime: 2020-03-07 12:04:18
*/
import axios from "axios";
import Cookies from "js-cookie";
axios.defaults.withCredentials = true;
const baseUrl = "/blog-backend";
export const api = axios.create({
// headers: { "Access-Control-Allow-Origin": "http://127.0.0.1:28080" },
// baseUrl
});
api.interceptors.request.use(
req => {
req.withCredentials = true;
console.log("req", req);
// Cookies.set(
// "api-uri",
// req.url.indexOf(baseUrl) > -1 ? req.url.substr(13) : null
// );
Cookies.set("front-url", window.location.href);
// req.headers["api-uri"] =
// req.url.indexOf(baseUrl) > -1 ? req.url.substr(13) : null;
// req.headers["front-url"] = window.location.href;
return req;
},
err => {
console.log("e", err);
if (401 === err.request.status) {
}
}
);
api.interceptors.response.use(
res => {
// if (res && res.request) {
// const responseURL = res.request.responseURL;
// if (
// responseURL ===
// "http://127.0.0.1:18080/cas/login?service=http://127.0.0.1:18081/blog-backend/shiro-cas"
// ) {
// const jsessionid = Cookies.get("JSESSIONID");
// console.log("jssionid", jsessionid);
// localStorage.setItem("jsessionid", jsessionid);
// window.location.href = `${responseURL}&test=1`;
// window.location.href = `${responseURL}`;
// }
// console.log("cookie", document.cookie);
// return res;
// }
return res;
},
err => {
console.log("e", err);
if (401 === err.response.status) {
const jsessionid = Cookies.get("JSESSIONID");
console.log("jssionid", jsessionid);
localStorage.setItem("jsessionid", jsessionid);
window.location.href =
"http://127.0.0.1:18080/cas/login?service=http://127.0.0.1:18081/blog-backend/shiro-cas";
}
}
);
## 时序图
用户未登录
```mermaid
sequenceDiagram
页面 ->> shiro客户端: 请求查询接口
Note right of shiro客户端: 没有认证AuthenticationFilter.isAuthenticated() false
FormAuthenticationFilter.onAccessDenied() 被拒绝
保存请求信息AccessControlFilter.saveRequest() 保存请求信息
AccessControlFilter.redirectToLogin() 重定向到登录页面
shiro客户端 -->> 页面:
页面 ->> cas服务: 重定向到cas服务 登录
Note right of cas服务: 获取TGC ST
登录
cas服务 -->> 页面:
页面 ->> shiro客户端: 携带ST
Note right of shiro客户端: 根据ST创建token
AuthenticatingFilter.createToken()
重定向到前端页面
shiro客户端 -->> 页面:
用户已登录
登录成功后重定向到前端的url是字符串时,下次访问不会再去cas server重新认证
当url是@Value获取的值时,下次访问会到cas server重新认证
详细问题