一、概述
org.geoserver.web.GeoServerBasePage 类,在Geoserver中是所有页面类的基类,也是单独存在的一个主UI界面入口文件。拿到源码后可以在里面进行肆意的魔改,也可以单独创建一个工程写根据它扩展。下面以登录的代码作为切入点,做了个简单的分析。
二、页面内容分析
2.1 本质
import org.apache.wicket.markup.html.WebPage;
public class GeoServerBasePage extends WebPage implements IAjaxIndicatorAware{
}
从类上能看出来是它本质上是个wicket页面
2.2 顶部logo以及登录区
2.2.1 页面logo
<link href="#" rel="shortcut icon" wicket:id="faviconLink"/>
if (faviconReference == null) {
faviconReference = new PackageResourceReference(GeoServerBasePage.class, "favicon.ico");
}
String faviconUrl = RequestCycle.get().urlFor(faviconReference, null).toString();
add(new ExternalLink("faviconLink", faviconUrl, null));
可以看出来logo使用的是同级的favicon.ico图片文件
2.2.2 登录区
在html页面中可以找到下面代码,是个wicket,不过主体就是个html
<div class="button-group selfclear">
<!--登录表单-->
<span wicket:id="loginforms">
<form style="display: inline-block;" wicket:id="loginform" method="post" action="../j_spring_security_check">
<span wicket:id="login.include"></span>
<button class="positive icon" type="submit">
<div><img src="#" wicket:id="link.icon"/><span wicket:id="link.label"></span></div>
</button>
<script type="text/javascript">
$('input, textarea').placeholder();
</script>
</form>
</span>
<!--注销表单-->
<span wicket:id="loggedinasform" class="username"><wicket:message key="loggedInAs">Logged in as</wicket:message> <span wicket:id="loggedInUsername">User von Testenheimer</span>.</span>
<form style="display: inline-block;" wicket:id="logoutform" method="post" action="j_spring_security_logout">
<button class="positive icon" type="submit">
<div><img src="#" wicket:id="link.icon"/><span wicket:id="link.label"></span></div>
</button>
</form>
<span id="localeSwitcher"><wicket:link><img src="img/icons/globe.png"/></wicket:link> <select wicket:id="localeSwitcher"></select></span>
</div>
登录表单和注销表单显示的前提条件是用户是否登录,登录的话就显示注销的按钮,没登录的就显示登录表单
在GeoServerBasePage.java中可以看到如下代码,获取当前登录用户并判断当前用户是否是游客权限
final Authentication user = GeoServerSession.get().getAuthentication();
final boolean anonymous = user == null || user instanceof AnonymousAuthenticationToken;
最终根据用户控制登录表单的显隐
// 登录显隐
loggedInAsForm.setVisible(!anonymous);
// 注销显隐
logoutForm.setVisible(!anonymous);
2.2.3 登录逻辑
在登录区看到登录表单显隐是根据用户以及用户权限控制,那么登录的逻辑是怎么样的呢
<form style="display: inline-block;" wicket:id="loginform" method="post" action="../j_spring_security_check">
</form>
从上面代码可以看到,登录的操作是在j_spring_security_check 中完成的,但它不是个直接的类方法,是spring中默认的登录逻辑,具体来说是spring security 也就是这个东西
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
据说spring security是个比较落后的技术了,我也没有深入研究过,毕竟geoserver开源了那么久了,有可能在当时也是个比较火的技术。关于spring security我看了一篇(博客)感觉写的不错,感兴趣的可以看看,大致的逻辑是通过重写spring security的登录逻辑实现登录,在geoserver的源码中的org.geoserver.security包中可以看到其具体逻辑
2.2.3.1 过滤器
当请求到达j_spring_security_check
时,它首先通过Spring Security的过滤器链,过滤器链中包含了一个或多个认证过滤器,如UsernamePasswordAuthenticationFilter
,它负责处理基于表单的认证,在spring 的配置文件中看到登录组件用的过滤器是GeoServerUserNamePasswordAuthenticationFilter
<bean id="geoserverFormLoginButton" class="org.geoserver.web.LoginFormInfo">
<property name="id" value="geoserverFormLoginButton" />
<!-- 组件需要的过滤器即登录过滤 -->
<property name="filterClass" value="org.geoserver.security.filter.GeoServerUserNamePasswordAuthenticationFilter" />
</bean>
2.2.3.2 认证管理器
过滤器设置了认证管理器AuthenticationManager(下面代码最后一行)
// add login filter
UsernamePasswordAuthenticationFilter filter =
new UsernamePasswordAuthenticationFilter() {
@Override
protected boolean requiresAuthentication(
HttpServletRequest request, HttpServletResponse response) {
for (String pathInfo : pathInfos) {
if (getRequestPath(request).startsWith(pathInfo)) return true;
}
return false;
}
};
filter.setPasswordParameter(upConfig.getPasswordParameterName());
filter.setUsernameParameter(upConfig.getUsernameParameterName());
filter.setAuthenticationManager(getSecurityManager().authenticationManager());
2.2.3.3 用户名密码认证
当用户通过登录表单提交用户名和密码时,这些信息会被UsernamePasswordAuthenticationFilter
捕获,
然后创建一个UsernamePasswordAuthenticationToken
对象,并将其传递给认证管理器AuthenticationManager
。然后会调用配置的AuthenticationProvider
,
private AuthenticationManager authMgrProxy =
new AuthenticationManager() {
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
return providerMgr.authenticate(authentication);
}
};
其中上述providerMgr就包括UsernamePasswordAuthenticationProvider
。UsernamePasswordAuthenticationProvider
的authenticate
方法被调用,以验证用户身份
src/main/java/org/geoserver/security/auth/UsernamePasswordAuthenticationProvider.java
public class UsernamePasswordAuthenticationProvider extends GeoServerAuthenticationProvider{
}
在UsernamePasswordAuthenticationProvider中有两个重点
- 设置UserDetailsService(获取文件、或者数据库中用户数据)
- 设置加密方式(设计密码的加密方式)
在初始化方法initializeFromConfig中找到下面代码
authProvider = new DaoAuthenticationProvider();
// 设置用户详情Service层
authProvider.setUserDetailsService(userDetailsService)
// 设置加密方式
authProvider.setPasswordEncoder(
new GeoServerMultiplexingPasswordEncoder(getSecurityManager(), ugService));
2.2.3.3.1 加密方式
关于密码的加密方式,其实geoserver的密码加密方式支持的类型很多(如MD5、SHA-1、BCrypt等),此处默认是混合类型,也就是同时支持多种加密,按理来说加密只能是一种,但geoserver怎么能同时支持多种呢,再往下查源码时就能看到
public String encodePassword(String rawPass, Object salt) throws UnsupportedOperationException {
for (GeoServerPasswordEncoder enc : encoders) {
try {
return enc.encodePassword(rawPass, salt);
} catch (Exception e) {
LOG.fine("Password encode failed with " + enc.getName());
}
}
throw new UnsupportedOperationException();
}
它会循环遍历支持的所有加密方法,会去假设每一种加密方式,直到匹配成功。
2.2.3.3.2 用户信息校验
那么继续看UsernamePasswordAuthenticationProvider
的authenticate
方法
@Override
public Authentication authenticate(Authentication authentication, HttpServletRequest request)
throws AuthenticationException {
UsernamePasswordAuthenticationToken auth = null;
try {
auth = (UsernamePasswordAuthenticationToken) authProvider.authenticate(authentication);
}
return auth;
}
里面的逻辑很少,主要还是调用了spring security 内置的DaoAuthenticationProvider的authenticate方法,反编译源码后看到图片中的代码
retrieveUser查用户详情的关键就是刚刚在UsernamePasswordAuthenticationProvider设置的UserDetailsService
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
return loadedUser;
}
}
下面那个验证用户名密码的就不说了,就是比较下数据库或者文件中查出来账号密码和用户传过来的是否匹配