引言
SSO 单点登录生成用户登入会话的通证,访问期间无需重复身份认证,即可根据 Session 和 Cookie 中保留的浏览器和服务器直接的交互数据,实现服务的持续安全访问。通过添加 sessionFilter 实现,可以处理自定义的登录认证方式。
SSO Configuration 配置 Bean
首先写入配置,注册 Filter,主要是通过 FilterRegistrationBean 注册 commonSessionFilter 和 localeResolverFilter 并指定过滤顺序。
@Configuration
@ConditionalOnProperty(prefix = "sso", name = "enabled", havingValue = "true", matchIfMissing = true)
public class SsoConfiguration {
@Bean
SystemConfig systemConfig(){
SystemConfig systemConfig = new SystemConfig() ;
systemConfig.init();
return systemConfig ;
}
// 注册filter
@Bean
public FilterRegistrationBean commonSessionFilter(){
FilterRegistrationBean registration = new FilterRegistrationBean();
CommonSessionFilter commonSessionFilter = new CommonSessionFilter() ;
registration.setName("commonSessionFilter");
registration.setFilter(commonSessionFilter);
registration.addUrlPatterns("/*");
registration.setOrder(2);
return registration;
}
@Bean
public FilterRegistrationBean localeResolverFilter(SystemConfig systemConfig){
FilterRegistrationBean registration = new FilterRegistrationBean();
LocaleResolverFilter localeResolverFilter = new LocaleResolverFilter() ;
registration.setName("localeResolverFilter");
registration.setFilter(localeResolverFilter);
registration.addUrlPatterns("/*");
registration.setOrder(3);
return registration;
}
@Bean
public BaseServiceContext baseServiceContext(@Value("${tenantApiKey}") String baseKey,
@Value("${tenantApiSecret}") String baseToken,
@Value("${sessionConfigHost}") String sessionConfigHost,
@Value("${tenantServiceRoute}") String tenantServiceRoute) {
BaseEnvironment baseEnvironment = new BaseEnvironment();
baseEnvironment.setBaseKey(baseKey);
baseEnvironment.setBaseToken(baseToken);
TenantEnvironment tenantEnvironment = new TenantEnvironment();
if(xxx){
tenantEnvironment.setHost(sessionConfigHost);
} else {
tenantEnvironment.setHost(tenantServiceRoute);
}
BaseServiceContext context = new BaseServiceContext();
context.setBaseEnvironment(baseEnvironment);
context.setTenantEnvironment(tenantEnvironment);
return context;
}
@Bean
public UserService interfaceInvokerProxyFactoryBean(BaseServiceContext baseServiceContext) {
return baseServiceContext.getService(UserService.class);
}
@Bean
public FilterRegistrationBean getContextFilterRegistrationBean(
@Autowired(required = false) UserService baseUserService
) {
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.addUrlPatterns("/*");
BaseUserFilter filter = new BaseUserFilter(baseUserService);
registrationBean.setFilter(filter);
registrationBean.setName("baseUserFilter");
registrationBean.setOrder(Ordered.LOWEST_PRECEDENCE);
StandardLoggingUtil.info(LOGGER,"BaseUserFilter init success");
return registrationBean;
}
// 触发构建PropertyUtils
@Bean
@ConditionalOnBean(Environment.class)
PropertyUtils propertyUtils(){
return new PropertyUtils() ;
}
}
CommonSession 获取用户上下文
@Component
@Order(5)
public class IamUserContextFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain)
throws ServletException, IOException {
String userName = null;
String tenantId = null;
if (httpServletRequest.getSession() instanceof CommonSession) {
CommonSession commonSession = (CommonSession) httpServletRequest.getSession();
if (SessionUtils.isLogin(commonSession)) {
userName = String.valueOf(commonSession.getAttribute(USERNAME));
tenantId = commonSession.getCustomSessionAttribute(TENANT_ID);
UserContextHolder.setUserContext(new UserContext(userName, tenantId));
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
filterChain.doFilter 按照指定顺序依次执行,如果都不能匹配上,会有一个 BaseUserFilter 兜底,返回 UserContext 推动系统下一步跳转登录或跳转服务。
header 携带验签信息
如果需要自定义访问认证和验签信息,可以用 HttpServletRequest 的 header 携带。包含验签部分的认证,需要额外做一步消息 body 的 cache 处理,因为消息体中数据只能取出一次,完成登录认证再到接口执行时取出会是空白。
@Component
@Order(value = Ordered.HIGHEST_PRECEDENCE)
@WebFilter(filterName = "RequestBodyCachingFilter", urlPatterns = "/*")
public class RequestBodyCachingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse resp,
FilterChain filterChain) throws ServletException, IOException {
String header = req.getHeader("x-header");
if (Objects.isNull(header) || !header.equals("true")) {
filterChain.doFilter(req, resp);
} else {
CachedBodyHttpServletRequest cachedBodyHttpServletRequest = new CachedBodyHttpServletRequest(req);
filterChain.doFilter(cachedBodyHttpServletRequest, resp);
}
}
public static class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
private final byte[] cachedBody;
public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
InputStream requestInputStream = request.getInputStream();
this.cachedBody = StreamUtils.copyToByteArray(requestInputStream);
}
@Override
public ServletInputStream getInputStream() {
return new CachedBodyServletInputStream(this.cachedBody);
}
@Override
public BufferedReader getReader() {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody);
return new BufferedReader(new InputStreamReader(byteArrayInputStream));
}
}
public static class CachedBodyServletInputStream extends ServletInputStream {
private final InputStream cachedBodyInputStream;
public CachedBodyServletInputStream(byte[] cachedBody) {
this.cachedBodyInputStream = new ByteArrayInputStream(cachedBody);
}
@Override
public boolean isFinished() {
try {
return cachedBodyInputStream.available() == 0;
} catch (IOException e) {
StandardLoggingUtil.error("cached body input stream " + e);
}
return false;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException();
}
@Override
public int read() throws IOException {
return cachedBodyInputStream.read();
}
}
}
改造 IamUserContextFilter,判断 header 并处理验签。验签过程:首先构造和上游相同的签名方法,然后用同一个密钥对消息生成签名,最后检验签名是否匹配。
// 构造签名内容
TreeMap<String, String> headerMap = new TreeMap<>();
headerMap.put("x-header", header);
headerMap.put("x-name", userName);
headerMap.put("x-access", accessId);
ByteString signatureContent;
try {
signatureContent = composeSignatureContent(headerMap, httpServletRequest);
} catch (IOException e) {
StandardLoggingUtil.error("compose signature content failed");
throw new ErrorCodeException(ErrorCodes.fromErrorTemplate(FAILED_TO_COMPOSE_SIGNATURE_CONTENT));
}
// 验签
String signature = httpServletRequest.getHeader("x-signature");
ByteString skByteString = ByteString.encodeUtf8(openapiSk);
if (!signatureContent.hmacSha256(skByteString).base64().equals(signature)) {
throw new ErrorCodeException(ErrorCodes.fromErrorTemplate(FAILED_TO_PASS_SIGNATURE_CHECK));
提供一种构造签名方法:
protected static ByteString composeSignatureContent(TreeMap<String, String> headerMap,
HttpServletRequest req) throws IOException {
TreeMap<String, String> queryParamMap = new TreeMap<>();
for (Map.Entry<String, String[]> entry : req.getParameterMap().entrySet()) {
queryParamMap.put(entry.getKey(), StringUtils.join(entry.getValue(), ","));
}
ByteString byteString = new Buffer().writeUtf8(req.getRequestURI())
.writeUtf8(connectParamMap(headerMap))
.writeUtf8(connectParamMap(queryParamMap))
.write(StreamUtils.copyToByteArray(req.getInputStream()))
.readByteString();
StandardLoggingUtil.info(LOGGER, "compose signature content " + byteString.utf8());
return byteString;
}
private static String connectParamMap(TreeMap<String, String> paramMap) {
List<String> items = new ArrayList<>();
for (Map.Entry<String, String> entry : paramMap.entrySet()) {
items.add(entry.getKey() + "=" + entry.getValue());
}
return org.apache.commons.lang3.StringUtils.join(items, "&");
}
总结
综上,SSO 提供了一种登录验证方法,并允许开发者通过改造 sessionFilter 自定义登录认证和验签方法。