在网上其实已经有很多案例,但热门主流的案例都不够全面,用起来其实还有不少问题,我记录下我在公司是如何修复这些问题的。
首先大部分网络上配置都直接写在Java里,但不利于维护,我个人比较喜欢写在配置文件里,以application.yml为例。
Maven引入三个包
<!--Apache Shiro所需的jar包 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.4</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.4</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-cas</artifactId>
<version>1.2.4</version>
</dependency>
在application.yml写入配置文件
cas:
#Cas服务器地址
casServerUrlPrefix: http://www.cas.com:8080/cas
#Cas登录页面地址
casLoginUrl: ${cas.casServerUrlPrefix}/login
#Cas登出页面地址
casLogoutUrl: ${cas.casServerUrlPrefix}/logout
#当前工程对外提供的服务地址
shiroServerUrlPrefix: http://localhost:8080
#casFilter UrlPattern
casFilterUrlPattern: /index
#登录地址shiroServerUrlPrefix
loginUrl: ${cas.casLoginUrl}?service=${cas.shiroServerUrlPrefix}${cas.casFilterUrlPattern}
#登出地址
logoutUrl: ${cas.casServerUrlPrefix}/logout
#登录成功跳转地址
loginSuccessUrl: /index
#权限认证失败跳转地址
unauthorizedUrl: /403
然后写一个读取该配置的Java文件,建议使用静态变量,方便引用
CasProp.java
@Component
@ConfigurationProperties(prefix="cas")
public class CasProp {
public static String casServerUrlPrefix;
// Cas登录页面地址
public static String casLoginUrl;
// Cas登出页面地址
public static String casLogoutUrl;
// 当前工程对外提供的服务地址
public static String shiroServerUrlPrefix;
// casFilter UrlPattern
public static String casFilterUrlPattern;
// 登录地址
public static String loginUrl;
// 登出地址
public static String logoutUrl;
// 登录成功地址
public static String loginSuccessUrl;
// 权限认证失败跳转地址
public static String unauthorizedUrl;
public String getCasServerUrlPrefix() {
return casServerUrlPrefix;
}
public void setCasServerUrlPrefix(String casServerUrlPrefix) {
this.casServerUrlPrefix = casServerUrlPrefix;
}
public String getCasLoginUrl() {
return casLoginUrl;
}
public void setCasLoginUrl(String casLoginUrl) {
this.casLoginUrl = casLoginUrl;
}
public String getCasLogoutUrl() {
return casLogoutUrl;
}
public void setCasLogoutUrl(String casLogoutUrl) {
this.casLogoutUrl = casLogoutUrl;
}
public String getShiroServerUrlPrefix() {
return shiroServerUrlPrefix;
}
public void setShiroServerUrlPrefix(String shiroServerUrlPrefix) {
this.shiroServerUrlPrefix = shiroServerUrlPrefix;
}
public String getCasFilterUrlPattern() {
return casFilterUrlPattern;
}
public void setCasFilterUrlPattern(String casFilterUrlPattern) {
this.casFilterUrlPattern = casFilterUrlPattern;
}
public String getLoginUrl() {
return loginUrl;
}
public void setLoginUrl(String loginUrl) {
this.loginUrl = loginUrl;
}
public String getLogoutUrl() {
return logoutUrl;
}
public void setLogoutUrl(String logoutUrl) {
this.logoutUrl = logoutUrl;
}
public String getLoginSuccessUrl() {
return loginSuccessUrl;
}
public void setLoginSuccessUrl(String loginSuccessUrl) {
this.loginSuccessUrl = loginSuccessUrl;
}
public String getUnauthorizedUrl() {
return unauthorizedUrl;
}
public void setUnauthorizedUrl(String unauthorizedUrl) {
this.unauthorizedUrl = unauthorizedUrl;
}
}
写一个ShiroConfig.java,这个文件是Shiro的配置文件,部分是SessionDao和redisCache,这两部份是可选加入的,不一定需要加入,看个人喜好,可选的地方均已标明,若引入时仍有错误,可酌情删除。
@Configuration
public class ShiroConfig {
private static final Logger logger = LoggerFactory.getLogger(RoleController.class);//可选开始
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.timeout}")
private int timeout;
@Value("${spring.cache.type}")
private String cacheType;
@Value("${server.session-timeout}")
private int tomcatTimeout;
//可选结束
@Bean(name = "myShiroCasRealm")
public MyShiroCasRealm myShiroCasRealm(EhCacheManager cacheManager) {
MyShiroCasRealm realm = new MyShiroCasRealm();
realm.setCacheManager(cacheManager);
realm.setCasServerUrlPrefix(CasProp.casServerUrlPrefix);
// 客户端回调地址
realm.setCasService(CasProp.loginSuccessUrl);
return realm;
}
//注册单点登出的listener
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
// 优先级需要高于Cas的Filter
public ServletListenerRegistrationBean<?> singleSignOutHttpSessionListener() {
ServletListenerRegistrationBean bean = new ServletListenerRegistrationBean();
bean.setListener(new SingleSignOutHttpSessionListener());
bean.setEnabled(true);
return bean;
}
@Bean
public FilterRegistrationBean singleSignOutFilter() {
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setName("singleSignOutFilter");
bean.setFilter(new SingleSignOutFilter());
bean.addUrlPatterns("/");
bean.setEnabled(true);
return bean;
}
/**
* 注册DelegatingFilterProxy(Shiro)
*
* @param
* @return
* @author SHANHY
* @create 2016年1月13日
*/
@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
filterRegistration.setFilter(new DelegatingFilterProxy("shiroFilter"));
// 该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理
filterRegistration.addInitParameter("targetFilterLifecycle", "true");
filterRegistration.setEnabled(true);
filterRegistration.addUrlPatterns("/*");
return filterRegistration;
}
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator daap = new DefaultAdvisorAutoProxyCreator();
daap.setProxyTargetClass(true);
return daap;
}
@Bean(name = "securityManager")
public SecurityManager getDefaultWebSecurityManager(MyShiroCasRealm myShiroCasRealm) {
DefaultWebSecurityManager dwsm = new DefaultWebSecurityManager();
dwsm.setRealm(myShiroCasRealm);
// 自定义缓存实现 使用redis,可选开始
if (Constant.CACHE_TYPE_REDIS.equals(cacheType)) {
dwsm.setCacheManager(rediscacheManager());
} else {
dwsm.setCacheManager(ehCacheManager());
logger.info("ehCachemanager--------->");
}// 自定义缓存实现 使用redis,可选结束
// <!-- 用户授权/认证信息Cache, 采用EhCache 缓存 -->
dwsm.setCacheManager(ehCacheManager());
// 指定 SubjectFactory
dwsm.setSubjectFactory(new CasSubjectFactory());
return dwsm;
}
@Bean
public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor aasa = new AuthorizationAttributeSourceAdvisor();
aasa.setSecurityManager(securityManager);
return aasa;
}
/**
* 加载shiroFilter权限控制规则(从数据库读取然后配置)
*
* @author SHANHY
* @create 2016年1月14日
*/
private void loadShiroFilterChain(ShiroFilterFactoryBean shiroFilterFactoryBean) {
/// 下面这些规则配置最好配置到配置文件中 ///
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
filterChainDefinitionMap.put(CasProp.casFilterUrlPattern, "casFilter");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/fonts/**", "anon");
filterChainDefinitionMap.put("/img/**", "anon");
filterChainDefinitionMap.put("/docs/**", "anon");
filterChainDefinitionMap.put("/druid/**", "anon");
filterChainDefinitionMap.put("/upload/**", "anon");
filterChainDefinitionMap.put("/files/**", "anon");
//filterChainDefinitionMap.put("/", "anon");
filterChainDefinitionMap.put("/blog", "anon");
filterChainDefinitionMap.put("/blog/open/**", "anon");
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
}
/**
* CAS过滤器
*
* @return
* @author SHANHY
* @create 2016年1月17日
*/
@Bean(name = "casFilter")
public CasFilter getCasFilter() {
CasFilter casFilter = new CasFilter();
casFilter.setName("casFilter");
casFilter.setEnabled(true);
// 登录失败后跳转的URL,也就是 Shiro 执行 CasRealm 的 doGetAuthenticationInfo 方法向CasServer验证tiket
casFilter.setFailureUrl(CasProp.loginUrl);// 我们选择认证失败后再打开登录页面
return casFilter;
}
/**
* ShiroFilter<br/>
* 注意这里参数中的 StudentService 和 IScoreDao 只是一个例子,因为我们在这里可以用这样的方式获取到相关访问数据库的对象,
* 然后读取数据库相关配置,配置到 shiroFilterFactoryBean 的访问规则中。实际项目中,请使用自己的Service来处理业务逻辑。
*
* @param
* @param
* @param
* @return
* @author SHANHY
* @create 2016年1月14日
*/
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(SecurityManager securityManager, CasFilter casFilter) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl(CasProp.loginUrl);
// 登录成功后要跳转的连接
// shiroFilterFactoryBean.setSuccessUrl(CasProp.loginSuccessUrl);
shiroFilterFactoryBean.setUnauthorizedUrl(CasProp.unauthorizedUrl);
// 添加casFilter到shiroFilter中
Map<String, Filter> filters = new HashMap<>();
filters.put("casFilter", casFilter);
shiroFilterFactoryBean.setFilters(filters);
loadShiroFilterChain(shiroFilterFactoryBean);
return shiroFilterFactoryBean;
}
@Bean
public EhCacheManager ehCacheManager() {
EhCacheManager em = new EhCacheManager();
em.setCacheManager(cacheManager());
return em;
}
@Bean("cacheManager2")
CacheManager cacheManager() {
return CacheManager.create();
}
/**
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* 配置shiro redisManager,可选
*
* @return
*/
@Bean
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(host);
redisManager.setPort(port);
redisManager.setExpire(1800);// 配置缓存过期时间
//redisManager.setTimeout(1800);
redisManager.setPassword(password);
return redisManager;
}
/**
* cacheManager 缓存 redis实现,可选
* 使用的是shiro-redis开源插件
*
* @return
*/
@Bean
public RedisCacheManager rediscacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
/**
* RedisSessionDAO shiro sessionDao层的实现 通过redis,可选
* 使用的是shiro-redis开源插件
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
//可选
@Bean
public SessionDAO sessionDAO() {
if (Constant.CACHE_TYPE_REDIS.equals(cacheType)) {
return redisSessionDAO();
} else {
return new MemorySessionDAO();
}
}
/**
* shiro session的管理,可选
*/
@Bean
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setGlobalSessionTimeout(tomcatTimeout * 1000);
sessionManager.setSessionDAO(sessionDAO());
Collection<SessionListener> listeners = new ArrayList<SessionListener>();
listeners.add(new BDSessionListener());
sessionManager.setSessionListeners(listeners);
return sessionManager;
}
}
写入MyShiroCasRealm.java,这里是用cas来做授权与认证的,酌情修改。
public class MyShiroCasRealm extends CasRealm{
private static final Logger logger = LoggerFactory.getLogger(MyShiroCasRealm.class);
// cas server地址
@Value("${cas.casServerUrlPrefix}")
public String casServerUrlPrefix;
// 登录成功地址
@Value("${cas.loginSuccessUrl}")
public String loginSuccessUrl;
@Autowired
CasProp casProp;
@Override
public void setCasServerUrlPrefix(String casServerUrlPrefix) {
this.casServerUrlPrefix = casServerUrlPrefix;
}
public void setLoginSuccessUrl(String loginSuccessUrl) {
this.loginSuccessUrl = loginSuccessUrl;
}
@PostConstruct
public void initProperty(){
super.setCasServerUrlPrefix(casServerUrlPrefix);
// 客户端回调地址
// setCasService(loginSuccessUrl);
}//这个方法用于加载权限
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) {
UserDO users = (UserDO)super.getAvailablePrincipal(arg0);
MenuService menuService = ApplicationContextRegister.getBean(MenuService.class);
Set<String> perms = menuService.listPerms(users.getUserId());
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setStringPermissions(perms);
super.onLogout(arg0);
logger.info("info:-------------->"+perms);
return info;
}
//这个方法用于认证用户身份
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
CasToken casToken = (CasToken) token;
if (token == null) {
return null;
}
String ticket = (String)casToken.getCredentials();
if (ticket == null || ticket.trim().length()==0) {
return null;
}
TicketValidator ticketValidator = ensureTicketValidator();
try {
// contact CAS server to validate service ticket
String url = casProp.getShiroServerUrlPrefix() + casProp.getCasFilterUrlPattern();//注意这里大坑,稍后说明
Assertion casAssertion = ticketValidator.validate(ticket,url);//"http://localhost:8080/index"
// get principal, user id and attributes
AttributePrincipal casPrincipal = casAssertion.getPrincipal();
String username = casPrincipal.getName();
// AuthenticationInfo authc = super.doGetAuthenticationInfo(token);
// if(authc==null) return null;
// String username = (String) authc.getPrincipals().getPrimaryPrincipal();
// String tokens = (String) authc.getCredentials();
Map<String, Object> map = new HashMap<>(16);
map.put("username", username);
UserDao userMapper = ApplicationContextRegister.getBean(UserDao.class);
// 查询用户信息
UserDO user = userMapper.list(map).get(0);
// 账号不存在
if (user == null) {
throw new UnknownAccountException("账号或密码不正确");
}
// 账号锁定
if (user.getStatus() == 0) {
throw new LockedAccountException("账号已被锁定,请联系管理员");
}
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, ticket, getName());
logger.info("info=" + info.toString());
return info;
}catch (TicketValidationException e){
e.printStackTrace();
throw new CasAuthenticationException("Unable to validate ticket [" + ticket + "]", e);
}
}
常见错误
org.jasig.cas.client.validation.TicketValidationException: 票根’ST-6-wv2-gk021lK8K7FfdgqvdhSRo-org-pc’不符合目标服务
这个错误耽误了两天最后才弄懂,网上的资料还是比较少说到,而且说到的好像也没解决这个问题,在这里我就说一下我的解决方案。
看到这个错误一般就是MyShiroCasRealm.java中的认证错误
Assertion casAssertion = ticketValidator.validate(ticket,url);
这个方法里面的url一定要和回调的url一致才可以
例如向Cas服务器发起登录,登录地址为 http://www.cas.com:8080/cas/login?service=http://localhost:8080/index
那么这个方法里填写的的ticketValidator.validate(ticket,url)的url为service后的url,http://localhost:8080/index
Assertion casAssertion = ticketValidator.validate(ticket,"http://localhost:8080/index");
需要这样填写才能正常识别,在网上似乎有别的说法,可能因版本不同会有别的写法,网上有人用的写法是
Assertion casAssertion = ticketValidator.validate(ticket,"http://localhost:8080/index/");
大家需要酌情尝试!!
资料参考:https://blog.csdn.net/yelllowcong/article/details/79290916