1.添加依赖包
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-cas</artifactId>
<version>1.10.1</version>
</dependency>
2.自定义Realm添加supports
原先采用的用户名、密码登录,添加supports支持账号密码验证
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
3.添加CasRealm,ShiroCasConfig,CasFilter
import okhttp3.Response;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.cas.CasAuthenticationException;
import org.apache.shiro.cas.CasRealm;
import org.apache.shiro.cas.CasToken;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.StringUtils;
import org.jasig.cas.client.authentication.AttributePrincipal;
import org.jasig.cas.client.validation.Assertion;
import org.jasig.cas.client.validation.TicketValidationException;
import org.jasig.cas.client.validation.TicketValidator;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import java.util.Date;
import java.util.List;
import java.util.Map;
public class MyShiroCasRealm extends CasRealm{
@Autowired
private UserRepository userRepository;
@Autowired
private UserService userService;
private static final org.slf4j.Logger logger = LoggerFactory.getLogger(MyShiroCasRealm.class);
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String username = (String) super.getAvailablePrincipal(principalCollection);
User user = userRepository.findByUsername(username);
if (null != user && user.getState() == 1) {
SimpleAuthorizationInfo authzInfo = new SimpleAuthorizationInfo();
// 业务处理
return authzInfo;
}
return null;
}
/**
* 1、CAS认证 ,验证用户身份
* 2、将用户基本信息设置到会话中
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
String username = null;
Map attributes = null;
String cn = null;
//将token转换为CasToken类型
CasToken casToken = (CasToken) token;
//如果token为空,则返回null
if (token == null) {
return null;
}
//获取票据
String ticket = (String)casToken.getCredentials();
//如果票据为空,则返回null
if (!StringUtils.hasText(ticket)) {
return null;
}
//确保票据验证器
TicketValidator ticketValidator = ensureTicketValidator();
try {
//验证票据是否有效
Assertion casAssertion = ticketValidator.validate(ticket, getCasService());
//获取返回信息
AttributePrincipal casPrincipal = casAssertion.getPrincipal();
username = casPrincipal.getName();
logger.debug("Validate ticket : {} in CAS server : {} to retrieve user : {}", ticket, getCasServerUrlPrefix(), username);
//获取配置的返回信息
attributes = casPrincipal.getAttributes();
cn = (String)attributes.get("cn");
} catch (TicketValidationException e) {
//如果验证异常,则抛出异常
throw new CasAuthenticationException("Unable to validate ticket [" + ticket + "]", e);
}
User user = userRepository.findByUsername(username);
String password = "******";
if(null == user){ // 新建用户
user = userService.createUser(username, cn==null?username:cn, password,0);
}
Subject subject = SecurityUtils.getSubject();
Session session = subject.getSession();
session.setAttribute("userId", user.getId());
PrincipalCollection principalCollection = new SimplePrincipalCollection(attributes, getName());
return new SimpleAuthenticationInfo(principalCollection, ticket);
}
}
认证策略:
import lombok.RequiredArgsConstructor;
import org.apache.shiro.cas.CasFilter;
import org.apache.shiro.cas.CasSubjectFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.session.SingleSignOutHttpSessionListener;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.web.filter.DelegatingFilterProxy;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Configuration
@AutoConfigureAfter(ShiroLifecycleBeanPostProcessorConfig.class)
@RequiredArgsConstructor
public class ShiroCasConfig {
final SystemConfigRepository systemConfigRepository;
String CasServerUrlPrefix = "http://127.0.0.1:8080/cas";
String ServerName = "http://127.0.0.1:8081";
// casFilter UrlPattern
public static final String casFilterUrlPattern = "/shiro-cas";
// Realm管理者
@Bean
public ModularRealmAuthenticator modularRealmAuthenticator(){
ModularRealmAuthenticator modularRealmAuthenticator = new ModularRealmAuthenticator();
//设置认证策略
modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
return modularRealmAuthenticator;
}
@Bean
public SecurityManager securityManager() {
// 创建一个默认的Web安全经理
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
// 设置Realm管理者(需要在设置Realm之前)
defaultWebSecurityManager.setAuthenticator(modularRealmAuthenticator());
// 指定Shiro的实例
// 在SecurityManager中新增多个realm
List<Realm> realms = new ArrayList<>();
realms.add(userRealm());
realms.add(myShiroCasRealm());
defaultWebSecurityManager.setRealms(realms);
// 指定Subject工厂,实现Cas免登录的功能
defaultWebSecurityManager.setSubjectFactory(new CasSubjectFactory());
DefaultWebSessionManagersessionManager=newDefaultWebSessionManager();
Collection<SessionListener>listeners=newArrayList<>();
//配置监听
listeners.add(sessionListener());
sessionManager.setSessionListeners(listeners);
defaultWebSecurityManager.setSessionManager(sessionManager);
// 返回默认的Web安全经理
return defaultWebSecurityManager;
}
@Bean("sessionListener")
public ShiroSessionListener sessionListener(){
return new ShiroSessionListener();
}
@Bean
public MyShiroCasRealm myShiroCasRealm() {
MyShiroCasRealm myShiroCasRealm = new MyShiroCasRealm();
// 设置cas登录服务器地址的前缀
myShiroCasRealm.setCasServerUrlPrefix(CasServerUrlPrefix);
// 客户端回调地址,登录成功后的跳转的地址(自己的服务器)
myShiroCasRealm.setCasService(ServerName+casFilterUrlPattern );
return myShiroCasRealm;
}
@Bean
public UserRealm userRealm() {
return new UserRealm();
}
// 注册单点登出的listener
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public ServletListenerRegistrationBean servletListenerRegistrationBean() {
//创建一个ServletListenerRegistrationBean对象
ServletListenerRegistrationBean bean = new ServletListenerRegistrationBean();
//设置监听器为SingleSignOutHttpSessionListener
bean.setListener(new SingleSignOutHttpSessionListener());
//开启监听器
bean.setEnabled(true);
//返回bean对象
return bean;
}
// 注册单点登出filter
@Bean
public FilterRegistrationBean registrationBean() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setName("registrationBean");
registrationBean.setFilter(new SingleSignOutFilter());
registrationBean.addUrlPatterns("/*"); //拦截所有的请求
registrationBean.setEnabled(true);
registrationBean.setOrder(10);//设置优先级
return registrationBean;
}
// 注册DelegatingFilterProxy(Shiro)
@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setFilter(new DelegatingFilterProxy("shiroFilter")); //设置的shiro的拦截器 ShiroFilterFactoryBean
bean.addInitParameter("targetFilterLifecycle", "true");
bean.setEnabled(true);
bean.addUrlPatterns("/*");
return bean;
}
// 下面两个配置主要用来开启shiro aop注解支持. 使用代理方式;所以需要开启代码支持;
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
// 设置代理方式,true是cglib的代理方式,false是普通的jdk代理方式
proxyCreator.setProxyTargetClass(true);
return proxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor attributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
// 开启注解
// 使用工厂模式,创建并初始化ShiroFilter
// CAS过滤器
// 定义一个CasFilter Bean,命名为casFilter
@Bean(name = "casFilter")
public CasFilter getCasFilter() {
// 创建一个MyCasFilter实例
MyCasFilter casFilter = new MyCasFilter();
// 设置CasFilter的名称
casFilter.setName("casFilter");
// 设置CasFilter的可用状态
casFilter.setEnabled(true);
// 设置CasFilter失败时的URL
casFilter.setFailureUrl(CasServerUrlPrefix + "/login?service=" + ServerName+"/proxyApi"+casFilterUrlPattern);
// 设置CasFilter成功时的URL
casFilter.setSuccessUrl(ServerName);
// 返回CasFilter实例
return casFilter;
}
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(SecurityManager securityManager, CasFilter casFilter) {
// 创建ShiroFilterFactoryBean实例
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
// 设置SecurityManager
factoryBean.setSecurityManager(securityManager);
// 设置登录URL
// 如果不设置,会自动寻找目录下的/login.jsp页面
factoryBean.setLoginUrl(CasServerUrlPrefix + "/login?service=" + ServerName+"/proxyApi"+casFilterUrlPattern);
// 设置成功访问URL
factoryBean.setSuccessUrl(ServerName);
// 设置无权限访问页面
// factoryBean.setUnauthorizedUrl(shiroServerUrlPrefix);
// 添加casFilter中,注意,casFilter需要放到shiroFilter的前面
factoryBean.getFilters().put("casFilter", casFilter);
factoryBean.getFilters().put("authc", new RestShiroAuthzFilter());
// 加载Shiro过滤链
loadShiroFilterChain(factoryBean);
return factoryBean;
}
private void loadShiroFilterChain(ShiroFilterFactoryBean shiroFilterFactoryBean) {
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
filterChainDefinitionMap.put("/shiro-cas", "casFilter");
//不拦截的请求
filterChainDefinitionMap.put("/oapi/**", "anon");
filterChainDefinitionMap.put("/api/anon/**", "anon");
filterChainDefinitionMap.put("/api/service/deploy/**", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
// 此处将logout页面设置为anon,而不是logout,因为logout被单点处理,而不需要再被shiro的logoutFilter进行拦截
filterChainDefinitionMap.put("/api/logout", "anon");
//登录过的不拦截
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
}
}
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.springframework.context.annotation.Bean;
public class ShiroLifecycleBeanPostProcessorConfig {
/**
* Shiro生命周期处理器
* 该类可以保证实现了org.apache.shiro.util.Initializable接口的shiro对象的init或者是destory方法被自动调用,
* 而不用手动指定init-method或者是destory-method方法
* 注意:如果使用了该类,则不需要手动指定初始化方法和销毁方法,否则会出错
* @return
*/
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
}
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.cas.CasFilter;
import org.apache.shiro.cas.CasToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.util.SavedRequest;
import org.apache.shiro.web.util.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMethod;
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.lang.reflect.Field;
public class MyCasFilter extends CasFilter {
@Autowired
private UserRepository userRepository;
private static final String TICKET_PARAMETER = "ticket";
private static final Logger logger = LoggerFactory.getLogger(MyCasFilter.class);
public MyCasFilter() {
}
@Override
public AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
// 获取请求的ticket
HttpServletRequest httpRequest = (HttpServletRequest) request;
String ticket = getRequestTicket(httpRequest);
if (StringUtils.isEmpty(ticket)) {
return null;
}
return new CasToken(ticket);
}
/**
* 拒绝除了option以外的所有请求
**/
@Override
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
logger.info("MyCasFilter isAccessAllowed");
return ((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name());
}
@Override
public boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
// 获取ticket,如果不存在,直接返回false
logger.info("MyCasFilter onAccessDenied");
String ticket = getRequestTicket((HttpServletRequest) request);
if (StringUtils.isEmpty(ticket)) {
return false;
}
return this.executeLogin(request, response);
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
//获取AuthenticationToken实体
AuthenticationToken token = createToken(request, response);
logger.info("MyCasFilter executeLogin");
if (token == null) {
String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " +
"must be created in order to execute a login attempt.";
throw new IllegalStateException(msg);
}
try {
Subject subject = getSubject(request, response);
//执行分子系统的Shrio认证与授权
subject.login(token);
SecurityUtils.getSubject().getSession().setTimeout(2400*1000);
SavedRequest shiroSavedRequest = (SavedRequest) SecurityUtils.getSubject().getSession(false).getAttribute("shiroSavedRequest");
if (shiroSavedRequest != null) {
//修改url地址为登录首页,否则会跳转到之前手输的地址容易404,由于没有set方法,所以使用反射
Class<? extends Object> clazz = shiroSavedRequest.getClass();
Field requestURI = clazz.getDeclaredField("requestURI");
requestURI.setAccessible(true);
requestURI.set(shiroSavedRequest,this.getSuccessUrl());
}
return onLoginSuccess(token, subject, request, response);
} catch (AuthenticationException e) {
return onLoginFailure(token, e, request, response);
}
}
/**
* 获取请求的ticket
*/
private String getRequestTicket(HttpServletRequest httpRequest) {
// 从参数中获取ticket
String ticket = httpRequest.getParameter(TICKET_PARAMETER);
if (StringUtils.isEmpty(ticket)) {
// 如果为空的话,则从header中获取参数
ticket = httpRequest.getHeader(TICKET_PARAMETER);
}
return ticket;
}
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request,
ServletResponse response) throws Exception {
logger.info("MyCasFilter onLoginSuccess");
boolean flag = true;
String successUrl = this.getSuccessUrl();
if("".equals(successUrl)){
successUrl = DEFAULT_SUCCESS_URL;
}
HttpServletResponse res = (HttpServletResponse) response;
HttpServletRequest req = (HttpServletRequest) request;
String username = req.getRemoteUser();
if(null != username){
User user = userRepository.findByUsername(username);
// 业务处理
}
WebUtils.issueRedirect(request, res, successUrl, null, flag);
//we handled the success redirect directly, prevent the chain from continuing:
return false;
}
}