简介
- 本项目将单点登录和权限认证结合在一起,实现了用户登录与授权的基础框架,后续可以很好的在此框架上进行二次开发
- 在原理和配置上也有很多不清楚的地方,希望大家留言讨论~
- 后续将会进行spring cloud系列工程的搭建,并将shiro-cas尝试接入spring cloud中
- spring cloud系列直达地址 spring cloud
单点登录流程
- 客户端请求目标服务器
- 目标服务器重定向到cas服务器
- cas服务器进行验证,通过则请求目标服务器,将ticket传给目标服务器
- 目标服务器根据ticket,请求cas服务器,获取用户登录信息
- cas服务器返回验证消息给目标服务器
项目实现
1、导入依赖包
<!--Apache Shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-cas</artifactId>
<version>${shiro.version}</version>
</dependency>
2、 编辑配置文件
# shiro - cas 配置
shiro:
# 在访问cas服务器登录之后,会返回一个ticket。由该地址接收
casFilterUrlPattern: /shiro-cas
# cas服务前缀
casServerUrlPrefix: http://127.0.0.1:8181/cas
# shiro服务前缀
shiroServerUrlPrefix: http://127.0.0.1:${server.port}${server.servlet.context-path}
# 登录地址
loginUrl: ${shiro.casServerUrlPrefix}/login?service=${shiro.shiroServerUrlPrefix}${shiro.casFilterUrlPattern}
# 登出地址
logoutUrl: ${shiro.casServerUrlPrefix}/logout?service=${shiro.shiroServerUrlPrefix}${shiro.casFilterUrlPattern}
# Tomcat
server:
tomcat:
uri-encoding: UTF-8
max-threads: 1000
min-spare-threads: 30
port: 40301
connection-timeout: 5000ms
servlet:
context-path: /shiro
session:
cookie:
http-only: true
spring:
application:
name: service-auth
# shiro - cas 配置
shiro:
# 在访问cas服务器登录之后,会返回一个ticket。由该地址接收
casFilterUrlPattern: /shiro-cas
# cas服务前缀
casServerUrlPrefix: http://127.0.0.1:8181/cas
# shiro服务前缀
shiroServerUrlPrefix: http://127.0.0.1:${server.port}${server.servlet.context-path}
# 登录地址
loginUrl: ${shiro.casServerUrlPrefix}/login?service=${shiro.shiroServerUrlPrefix}${shiro.casFilterUrlPattern}
# 登出地址
logoutUrl: ${shiro.casServerUrlPrefix}/logout?service=${shiro.shiroServerUrlPrefix}${shiro.casFilterUrlPattern}
3、自定义配置casFileter
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.cas.CasFilter;
import org.apache.shiro.cas.CasToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
/**
* @author : xingsongtan@qq.com
* @date : 14:17 2019/7/17
*/
public class MyCasFilter extends CasFilter {
private static Logger logger = LoggerFactory.getLogger(MyCasFilter.class);
private static final String TICKET_PARAMETER = "ticket";
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)) {
logger.debug("票证获取失败,票证为空!");
return null;
}
return new CasToken(ticket);
}
/**
* 拒绝除了option以外的所有请求
**/
@Override
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {
return true;
}
return false;
}
@Override
public boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
// 获取ticket,如果不存在,直接返回false
String ticket = getRequestTicket((HttpServletRequest) request);
if (StringUtils.isEmpty(ticket)) {
return false;
}
return this.executeLogin(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;
}
}
4、自定义casRealm
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashSet;
import java.util.Set;
/**
* @author : xingsongtan@qq.com
* @date : 12:00 2019/7/17
*/
public class MyCasRealm extends CasRealm {
private static Logger log = LoggerFactory.getLogger(MyCasRealm.class);
/**
* 在调用subject.login()时,首先调用此接口
*/
@Override
public AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 调用父类的方法,然后授权用户
AuthenticationInfo authc = super.doGetAuthenticationInfo(token);
// 获得用户名
String username = (String) authc.getPrincipals().getPrimaryPrincipal();
// TODO:这里应该从数据库中获取用户信息
return authc;
}
/**
* 进行权限验证的时候,调用方法,将用户的权限信息写进SimpleAuthorizationInfo
*/
@Override
public AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 用户名称
log.info("进入了权限认证");
Object username = principals.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// TODO: 这里应该从数据库获取用户权限
Set<String> permission = new HashSet<>();
permission.add("sys:dept:list");
info.setStringPermissions(permission);
return info;
}
}
6、进行shiroconfig配置
import com.ttcode.shiro.security.MyCasFilter;
import com.ttcode.shiro.security.MyCasRealm;
import org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.cas.CasSubjectFactory;
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.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.DelegatingFilterProxy;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Shiro的配置文件
*
* @author : xingsongtan@qq.com
* @date : 20:51 2019/7/18
*/
@Configuration
public class ShiroCasConfiguration {
/**
* 添加shiro的filter
*/
@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
filterRegistration.setFilter(new DelegatingFilterProxy("shiroFilter"));
filterRegistration.addInitParameter("targetFilterLifecycle", "true");
filterRegistration.setEnabled(true);
filterRegistration.addUrlPatterns("/*");
return filterRegistration;
}
/**
* 保证了shiro内部lifecycle函数bean的执行
*/
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 配置授权策略
*/
@Bean(name = "authenticator")
public ModularRealmAuthenticator modularRealmAuthenticator() {
ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
return authenticator;
}
@Bean(name = "casRealm")
public MyCasRealm casRealm(@Value("${shiro.casServerUrlPrefix}") String casServerUrlPrefix,
@Value("${shiro.shiroServerUrlPrefix}") String shiroServerUrlPrefix,
@Value("${shiro.casFilterUrlPattern}") String casFilterUrlPattern) {
MyCasRealm casRealm = new MyCasRealm();
// 认证通过后的默认角色
casRealm.setDefaultRoles("ROLE_USER");
// cas服务端地址前缀
casRealm.setCasServerUrlPrefix(casServerUrlPrefix);
// 应用服务地址,用来接收cas服务端票证
casRealm.setCasService(shiroServerUrlPrefix + casFilterUrlPattern);
return casRealm;
}
/**
* 配置安全管理器
**/
@Bean(name = "securityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(ModularRealmAuthenticator authenticator,
MyCasRealm casRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置授权策略,此步骤必须在设置realm的前面,不然会报错realm未配置
securityManager.setAuthenticator(authenticator);
securityManager.setSubjectFactory(new CasSubjectFactory());
// 缓存管理器
securityManager.setCacheManager(new MemoryConstrainedCacheManager());
// 设置自定义验证策略
securityManager.setRealm(casRealm);
return securityManager;
}
/**
* 配置登录过滤器
*/
@Bean(name = "casFilter")
public MyCasFilter casFilter(@Value("${shiro.loginUrl}") String loginUrl) {
MyCasFilter casFilter = new MyCasFilter();
casFilter.setName("casFilter");
casFilter.setEnabled(true);
casFilter.setFailureUrl(loginUrl);
return casFilter;
}
/**
* shiro 过滤器
*/
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager,
MyCasFilter casFilter,
@Value("${shiro.logoutUrl}") String logoutUrl,
@Value("${shiro.loginUrl}") String loginUrl,
@Value("${shiro.casFilterUrlPattern}") String casFilterUrlPattern) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 设置安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 设置登录地址
shiroFilterFactoryBean.setLoginUrl(loginUrl);
// 设置登录成功地址
shiroFilterFactoryBean.setSuccessUrl("/");
// 配置拦截地址
Map<String, Filter> filters = new HashMap<>();
filters.put("casFilter", casFilter);
LogoutFilter logoutFilter = new LogoutFilter();
// 配置登出地址
logoutFilter.setRedirectUrl(logoutUrl);
filters.put("logout", logoutFilter);
shiroFilterFactoryBean.setFilters(filters);
// 设置访问用户页面需要授权的操作
loadShiroFilterChain(shiroFilterFactoryBean, casFilterUrlPattern);
// 将设置的权限设置到shiroFilterFactoryBean
return shiroFilterFactoryBean;
}
/**
* 1、当我们第一次访问客户端时,先去cas进行认证,成功后会返回一个ticket
* 2、返回的ticket地址在casRealm已经进行了配置,shiroServerUrlPrefix + casFilterUrlPattern
* 3、即地址为/shiro-cas,对该地址进行casFilter拦截
*/
private void loadShiroFilterChain(ShiroFilterFactoryBean shiroFilterFactoryBean, String casFilterUrlPattern) {
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put(casFilterUrlPattern, "casFilter");
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
}
/**
* 开启Shiro的注解(如@RequiresPermissions)
* 需借助SpringAOP扫描使用Shiro注解的类
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
*
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 开启aop注解支持
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
结尾:以上为核心配置