cas实现单点登录过程(二)
上次说过了cas的实现整个流程,现在说一下cas的代码实现方式。
我这里使用的shiro+cas的方式进行实现的,生成token的方式,这里是一个事例具体得要根据你自己的项目进行改造。
也可以根据我的cas实现过程修改后使用在spring-security中
前提是你本地要搭建cas server,我本地已经搭建完成。
直接上代码
添加shiro与cas的maven依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-cas</artifactId>
<version>1.4.0</version>
</dependency>
ShiroConfig的配置
/**
* @description: shiro配置类
*/
@Configuration
public class ShiroConfiguration {
@Autowired
private JwtTokenUtil jwtTokenUtil;
/**
* Shiro Realm 继承自AuthorizingRealm的自定义Realm,即指定Shiro验证用户登录的类为自定义的
*/
@Bean(name = "userRealm")
public UserRealm userRealm() {
UserRealm userRealm = new UserRealm();
userRealm.setCachingEnabled(false);
return userRealm;
}
@Bean
public FilterRegistrationBean delegatingFilterProxy() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
DelegatingFilterProxy proxy = new DelegatingFilterProxy();
proxy.setTargetFilterLifecycle(true);
proxy.setTargetBeanName("shiroFilter");
filterRegistrationBean.setFilter(proxy);
return filterRegistrationBean;
}
/**
* Shiro的Web过滤器Factory 命名:shiroFilter
*/
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
//Shiro的核心安全接口,这个属性是必须的
shiroFilter.setLoginUrl("/cas/login");
shiroFilter.setSecurityManager(securityManager);
Map<String, Filter> filters = new LinkedHashMap<>();
CasCallbackFilter casCallbackFilter = new CasCallbackFilter();
casCallbackFilter.setCasServerUrlPrefix("https://localhost:8443/cas-server-webapp-tomcat-5.3.14");
casCallbackFilter.setCasService("http://localhost:8999/cas/callback");
casCallbackFilter.setJwtTokenUtil(jwtTokenUtil);
filters.put("casCallback", casCallbackFilter);
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter();
jwtAuthenticationFilter.setRedirectUrl("https://localhost:8443/cas-server-webapp-tomcat-5.3.14/login?service=http://localhost:8999/cas/callback");
filters.put("authc", jwtAuthenticationFilter);
shiroFilter.setFilters(filters);
/*定义shiro过滤链 Map结构
* Map中key(xml中是指value值)的第一个'/'代表的路径是相对于HttpServletRequest.getContextPath()的值来的
* anon:它对应的过滤器里面是空的,什么都没做,这里.do和.jsp后面的*表示参数,比方说login.jsp?main这种
* authc:该过滤器下的页面必须验证后才能访问,它是Shiro内置的一个拦截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter
*/
Map<String, String> filterMap = new LinkedHashMap<>();
/* 过滤链定义,从上向下顺序执行,一般将 / ** 放在最为下边:这是一个坑呢,一不小心代码就不好使了;
authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问 */
filterMap.put("/", "anon");
filterMap.put("/static/**", "anon");
filterMap.put("/login/auth", "anon");
filterMap.put("/login/logout", "anon");
filterMap.put("/error", "anon");
filterMap.put("/cas/callback", "casCallback");
filterMap.put("/**", "authc");
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
/**
* Subject工厂管理器
* @return
*/
@Bean
public DefaultWebSubjectFactory subjectFactory(){
DefaultWebSubjectFactory subjectFactory = new StatelessDefaultSubjectFactory();
return subjectFactory;
}
/**
* 不指定名字的话,自动创建一个方法名第一个字母小写的bean
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm());
// 替换默认的DefaultSubjectFactory,用于关闭session功能
securityManager.setSubjectFactory(subjectFactory());
securityManager.setSessionManager(sessionManager());
// 关闭session存储,禁用Session作为存储策略的实现,但它没有完全地禁用Session所以需要配合SubjectFactory中的context.setSessionCreationEnabled(false)
((DefaultSessionStorageEvaluator) ((DefaultSubjectDAO)securityManager.getSubjectDAO()).getSessionStorageEvaluator()).setSessionStorageEnabled(false);
SecurityUtils.setSecurityManager(securityManager);
return securityManager;
}
/**
* 会话管理器
* @return
*/
public DefaultSessionManager sessionManager(){
DefaultSessionManager sessionManager =new DefaultSessionManager();
// 关闭session定时检查,通过setSessionValidationSchedulerEnabled禁用掉会话调度器
sessionManager.setSessionValidationSchedulerEnabled(false);
return sessionManager;
}
/**
* 用户授权信息缓存
* @return
*/
@Bean
public CacheManager cacheManager() {
return new MemoryConstrainedCacheManager();
}
/**
* 凭证匹配器
* (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
* 所以我们需要修改下doGetAuthenticationInfo中的代码;
* )
* 可以扩展凭证匹配器,实现 输入密码错误次数后锁定等功能,下一次
*/
@Bean
public CredentialsMatcher CredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
//散列的次数,比如散列两次,相当于 md5(md5(""));
hashedCredentialsMatcher.setHashIterations(1024);
//storedCredentialsHexEncoded默认是true,此时用的是密码加密用的是Hex编码;false时用Base64编码
hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
return hashedCredentialsMatcher;
}
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}
}
JwtAuthenticationFilter 执行URL的转发跳转到cas server中认证 拿取token做
public class JwtAuthenticationFilter extends AuthenticatingFilter {
private final Logger LOGGER = LoggerFactory.getLogger(getClass());
private String redirectUrl;
public void setRedirectUrl(String redirectUrl) {
this.redirectUrl = redirectUrl;
}
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse servletResponse) throws Exception {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 先从Header里面获取 前端将token放置在header中
String token = httpRequest.getHeader(Constant.TOKEN);
if(StringUtils.isEmpty(token)){
// 获取不到再从Parameter中拿
token = httpRequest.getParameter(Constant.TOKEN);
// 还是获取不到再从Cookie中拿
if(StringUtils.isEmpty(token)){
Cookie[] cookies = httpRequest.getCookies();
if(cookies != null){
for (Cookie cookie : cookies) {
if(Constant.TOKEN.equals(cookie.getName())){
token = cookie.getValue();
break;
}
}
}
}
}
JwtToken jwtToken = new JwtToken();
jwtToken.setToken(token);
return jwtToken;
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
if (isLoginRequest(request,response)) {
// TODO 跳转到认证服务 cas server带上回调地址 便于登录认证成功后回调到自己的服务
((HttpServletResponse) response).sendRedirect("https://localhost:8443/cas-server-webapp-tomcat-5.3.14/login?service=http://localhost:8999/cas/callback");
}
return super.executeLogin(request, response);
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
//必须设置要不不能得到token 因为这里是自动以的header所以要进行设置
if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {
return true;
}
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest)request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
// 获取请求token,如果token不存在,直接返回401
String token = getRequestToken((HttpServletRequest) request);
if (StringUtils.isBlank(token)) {
return executeLogin(request, response);
}
try {
return executeLogin(request, response);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request,
ServletResponse response) throws Exception {
return true;
}
/**
* 获取请求的token
*/
private String getRequestToken(HttpServletRequest httpRequest) {
// 从header中获取token
String token = httpRequest.getHeader("token");
// 如果header中不存在token,则从参数中获取token
if (StringUtils.isBlank(token)) {
token = httpRequest.getParameter("token");
}
if (StringUtils.isBlank(token)) {
token = httpRequest.getParameter("sx_sso_sessionid");
}
return token;
}
}
StatelessDefaultSubjectFactory
/**
* 通过调用context.setSessionCreationEnabled(false)表示不创建会话,
* 如果之后调用Subject.getSession()将抛出DisabledSessionException异常。
*/
public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory {
@Override
public Subject createSubject(SubjectContext context) {
// 不创建session
context.setSessionCreationEnabled(false);
return super.createSubject(context);
}
}
CasCallbackFilter 从cas server中拿取ticket并验证是否正确过期,并生成token,cas server登出销毁token
public class CasCallbackFilter extends PathMatchingFilter {
private JwtTokenUtil jwtTokenUtil;
private TicketValidator ticketValidator;
private String casService;
private String casServerUrlPrefix;
public String getCasServerUrlPrefix() {
return casServerUrlPrefix;
}
public void setCasServerUrlPrefix(String casServerUrlPrefix) {
this.casServerUrlPrefix = casServerUrlPrefix;
}
public String getCasService() {
return casService;
}
public void setCasService(String casService) {
this.casService = casService;
}
public JwtTokenUtil getJwtTokenUtil() {
return jwtTokenUtil;
}
public void setJwtTokenUtil(JwtTokenUtil jwtTokenUtil) {
this.jwtTokenUtil = jwtTokenUtil;
}
public CasCallbackFilter() {
setSuffix(Constant.DEFAULT_CALLBACK_SUFFIX);
}
protected TicketValidator ensureTicketValidator() {
if (this.ticketValidator == null) {
this.ticketValidator = createTicketValidator();
}
return this.ticketValidator;
}
protected TicketValidator createTicketValidator() {
String urlPrefix = getCasServerUrlPrefix();
return new Cas20ServiceTicketValidator(urlPrefix);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if (mustApply(servletRequest)){
HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
String ticket = "";
//判断ticket是否为空
if (isTokenRequest(httpRequest)){
ticket = httpRequest.getParameter(Constant.TICKET_NAME);
Assertion casAssertion = null;
try {
ticketValidator = ensureTicketValidator();
casAssertion = ticketValidator.validate(ticket, getCasService());
// get principal, user id and attributes
AttributePrincipal casPrincipal = casAssertion.getPrincipal();
String username = casPrincipal.getName();
// 验证用户名密码成功后生成token
String token = jwtTokenUtil.generateToken(username);
//TODO 保存ticket与token
//跳转到前端服务并携带token
((HttpServletResponse) servletResponse).sendRedirect("http://localhost:9520/#/?token=" + token);
} catch (TicketValidationException e) {
e.printStackTrace();
}
} else if (isBackLogoutRequest(httpRequest)) {
//得到登出标识
final String logoutMessage = httpRequest.getParameter(Constant.LOGOUT_NAME);
//解析得到ticket
ticket = substring(logoutMessage,"SessionIndex>","</");
//TODO 在数据库中删除ticket
}
}else {
filterChain.doFilter(servletRequest, servletResponse);
}
}
@Override
public void destroy() {
}
private boolean isBackLogoutRequest(HttpServletRequest httpRequest) {
if (StringUtils.equalsAnyIgnoreCase(HttpMethod.POST.name(), httpRequest.getMethod())){
String contentType = "";
Enumeration<String> headerNames = httpRequest.getHeaderNames();
while ( headerNames.hasMoreElements()){
String headerName = headerNames.nextElement();
if (StringUtils.equalsAnyIgnoreCase(headerName,Constant.CONTENT_TYPE_NAME)) {
contentType = httpRequest.getHeader(headerName);
if ( !StringUtils.startsWithIgnoreCase(contentType,Constant.TICKET_MULTIPART)){
if (StringUtils.isNoneEmpty(httpRequest.getParameter(Constant.LOGOUT_NAME))){
return Boolean.TRUE;
}
}
}
}
}
return Boolean.FALSE;
}
public static String substring(final String str, final String open, final String close) {
if (str == null || open == null || close == null) {
return null;
}
int start = str.indexOf(open);
if (start != Constant.INDEX_NOT_FOUND) {
int end = str.indexOf(close, start + open.length());
if (end != Constant.INDEX_NOT_FOUND) {
return str.substring(start + open.length(), end);
}
}
return null;
}
private boolean isTokenRequest(HttpServletRequest httpRequest) {
return StringUtils.isNoneEmpty(httpRequest.getParameter(Constant.TICKET_NAME));
}
}
PathMatchingFilter
public abstract class PathMatchingFilter implements Filter {
protected final Logger logger = LoggerFactory.getLogger(getClass());
private String suffix;
protected boolean mustApply(final ServletRequest request) {
final String path = getPath((HttpServletRequest)request);
logger.debug("path: {} | suffix: {}", path, suffix);
if (StringUtils.isEmpty(suffix)) {
return true;
} else {
return path != null && path.endsWith(suffix);//判断后缀是否相等 相等返回true
}
}
public String getPath(HttpServletRequest request) {
final String fullPath = request.getRequestURI();
// it shouldn't be null, but in case it is, it's better to return empty string
if (fullPath == null) {
return "";
}
final String context = request.getContextPath();
// this one shouldn't be null either, but in case it is, then let's consider it is empty
if (context != null) {
return fullPath.substring(context.length());
}
return fullPath;
}
public String getSuffix() {
return suffix;
}
public void setSuffix(final String suffix) {
this.suffix = suffix;
}
}
JwtToken
public class JwtToken implements AuthenticationToken {
private String principal;
private String token;
@Override
public String getPrincipal() {
return principal;
}
@Override
public Object getCredentials() {
return token;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public JwtToken() {
}
public JwtToken(String principal, String token) {
this.principal = principal;
this.token = token;
}
}
/**
* @description: 自定义Realm
*/
public class UserRealm extends AuthorizingRealm {
private Logger logger = LoggerFactory.getLogger(UserRealm.class);
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private LoginService loginService;
@Override
public boolean supports(AuthenticationToken token) {
//表示此Realm只支持JwtToken类型
return token instanceof JwtToken;
}
@Override
@SuppressWarnings("unchecked")
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 根据用户名查找角色,请根据需求实现
String username = (String)principals.getPrimaryPrincipal();
Session session = SecurityUtils.getSubject().getSession();
//查询用户的权限
JSONObject permission = (JSONObject) session.getAttribute(Constant.SESSION_USER_PERMISSION);
logger.info("permission的值为:" + permission);
logger.info("本用户权限为:" + permission.get("permissionList"));
//为当前用户设置角色和权限
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.addStringPermissions((Collection<String>) permission.get("permissionList"));
return authorizationInfo;
}
/**
* 验证当前登录的Subject
* LoginController.login()方法中执行Subject.login()时 执行此方法
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {
JwtToken jwtToken = (JwtToken) authcToken;
// 获取token
String token = jwtToken.getToken();
String username = jwtTokenUtil.getUsernameFromToken(token);
// 获取用户密码
//String password = new String((char[]) authcToken.getCredentials());
JSONObject user = loginService.getUser(username);
if (user == null) {
//没找到帐号
throw new UnknownAccountException();
}
//交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配,如果觉得人家的不好可以自定义实现
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
user.getString("username"),
token,
//user.getString("password"),
//ByteSource.Util.bytes("salt"), salt=username+salt,采用明文访问时,不需要此句
getName()
);
//session中不需要保存密码
//user.remove("password");
//将用户信息放入session中
SecurityUtils.getSubject().getSession().setAttribute(Constant.SESSION_USER_INFO, user);
return authenticationInfo;
}
@Override
public void clearCachedAuthorizationInfo(PrincipalCollection principals) {
super.clearCachedAuthorizationInfo(principals);
}
@Override
public void clearCachedAuthenticationInfo(PrincipalCollection principals) {
super.clearCachedAuthenticationInfo(principals);
}
@Override
public void clearCache(PrincipalCollection principals) {
super.clearCache(principals);
}
public void clearAllCachedAuthorizationInfo() {
getAuthorizationCache().clear();
}
public void clearAllCachedAuthenticationInfo() {
getAuthenticationCache().clear();
}
public void clearAllCache() {
clearAllCachedAuthenticationInfo();
clearAllCachedAuthorizationInfo();
}
}
@Component
public class JwtTokenUtil {
@Value("${jwt.token.secret}")
private String secret;
@Value("${jwt.token.expiration}")
private Long expiration;
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public Long getExpiration() {
return expiration;
}
public void setExpiration(Long expiration) {
this.expiration = expiration;
}
private Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
/**
* 生成token
* @param username 用户名
* @param
* @return
*/
public String generateToken(String username) {
Map<String, Object> claims = new HashMap<>();
claims.put(Constant.CLAIM_KEY_USERNAME, username);
// claims.put(CLAIM_KEY_AUDIENCE, generateAudience(device));
claims.put(Constant.CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
private String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, this.secret)
.compact();
}
/**
* 生成token时间 = 当前时间 + expiration(properties中配置的失效时间)
* @return
*/
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + expiration * 1000);
}
// /**
// * 通过spring-mobile-device的device检测访问主体
// * @param device
// * @return
// */
// private String generateAudience(Device device) {
// String audience = AUDIENCE_UNKNOWN;
// if (device.isNormal()) {
// audience = AUDIENCE_WEB;//PC端
// } else if (device.isTablet()) {
// audience = AUDIENCE_TABLET;//平板
// } else if (device.isMobile()) {
// audience = AUDIENCE_MOBILE;//手机
// }
// return audience;
// }
/**
* 根据token获取用户名
* @param token
* @return
*/
public String getUsernameFromToken(String token) {
String username;
try {
final Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 判断token失效时间是否到了
* @param token
* @return
*/
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
/**
* 获取设置的token失效时间
* @param token
* @return
*/
public Date getExpirationDateFromToken(String token) {
Date expiration;
try {
final Claims claims = getClaimsFromToken(token);
expiration = claims.getExpiration();
} catch (Exception e) {
expiration = null;
}
return expiration;
}
// /**
// * Token失效校验
// * @param token token字符串
// * @param loginInfo 用户信息
// * @return
// */
// public Boolean validateToken(String token, LoginInfo loginInfo) {
// final String username = getUsernameFromToken(token);
// return (
// username.equals(loginInfo.getUsername())
// && !isTokenExpired(token));
// }
public String refreshToken(String token) {
final Claims claims = this.getClaimsFromToken(token);
claims.put(Constant.CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
}
public class Constant {
public static final String SUCCESS_CODE = "100";
public static final String SUCCESS_MSG = "请求成功";
/**
* session中存放用户信息的key值
*/
public static final String SESSION_USER_INFO = "userInfo";
public static final String SESSION_USER_PERMISSION = "userPermission";
/*------------------------------------------------cas--------------------------------------*/
public static final String CLAIM_KEY_USERNAME = "sub";
public static final String CLAIM_KEY_AUDIENCE = "audience";
public static final String CLAIM_KEY_CREATED = "created";
public static final String AUDIENCE_UNKNOWN = "unknown";
public static final String AUDIENCE_WEB = "web";
public static final String AUDIENCE_MOBILE = "mobile";
public static final String AUDIENCE_TABLET = "tablet";
public static final String TOKEN = "token";
public final static String DEFAULT_CALLBACK_SUFFIX = "/callback";
public static final String TICKET_NAME = "ticket";
public static final String TICKET_MULTIPART = "multipart";
public static final String CONTENT_TYPE_NAME = "Content-Type";
public static final String LOGOUT_NAME = "logoutRequest";
public static final int INDEX_NOT_FOUND = -1;
}