title: Shiro实现多域名登录界面 tags:
- shiro
- dns
- 多域名
- domain categories: shiro date: 2017-08-21 18:18:52
背景
目前开发接到需求如下,希望根据不同用户实现自定义域名登录(前台ui等需要略微区分,配色,皮肤等)
现状
目前系统中使用shiro作为授权权限框架,当用户没有登录时将会默认返回未授权页
比如
<!-- 配置shiroFilter-->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="${wxb.url}"/>
<property name="successUrl" value="/kzf6/page/index/index.jsp" />
<property name="unauthorizedUrl" value="/kzf6/page/error/403.jsp" />
<property name="filters">
<map>
<entry key="kickout" value-ref="kickoutSessionControlFilter"/>
</map>
</property>
<property name="filterChainDefinitions">
<value>
/mlogin/login.json = anon
<!-- 除了上面定义的url和资源,都需要配认证后才可以访问 -->
/** = kickout,authc
</value>
</property>
</bean>
复制代码
上述配置可以导致未登录用户来自动重定向到${wxb.url}
那么我们现在需要自定义多个(目前是已知url,可以在发布时配置死)
方案
为了尽量减少部署成本(因此可以将新的域名CNAME到原来域名或者A记录)
一、A记录、CNAME和URL区别
它们间区别如下:
- A记录 —— 映射域名到一个或多个IP。
- CNAME——映射域名到另一个域名(子域名)。
- URL转发——重定向一个域名到另一个URL地址,使用HTTP 301状态码。
A记录、CNAME解析时都将先解析到IP地址。而URL则只是重定向转发。CNAME可以随意设,但URL转发在一些缺少网络自由的国家是被禁止的,因为URL转发还分显示和隐式,很容易造成误解。
注意,无论是A记录、CNAME、URL转发,在实际使用时是全部可以设置多条记录的。比如:
- ftp.example.com A记录到 IP1,而mail.example.com则A记录到IP2
- ftp.example.com CNAME到 ftp.abc.com,而mail.example.com则CNAME到mail.abc.com
- ftp.example.com 转发到 ftp.abc.com,而mail.example.com则A记录到mail.abc.com
二、A记录、CNAME、URL适用范围
了解以上区别,在应用方面:
- A记录——适应于独立主机、有固定IP地址
- CNAME——适应于虚拟主机、变动IP地址主机
- URL转发——适应于更换域名又不想抛弃老用户
因此实质上和原先服务器完全相同,唯一区别是用户访问到服务器上获取的servername发生了变化
由此我们可以根据servername来做用户的划分(提供多套ui)
问题
- 解决多域名登录跳转
- 解决登录后用户强制退出跳转页面
解决方案
扩展AuthFIlter
package com.air.tqb.shiro.filter;
import com.air.tqb.common.LoginDomain;
import com.air.tqb.mapper.base.MenuMapper;
import com.air.tqb.utils.WxbStatic;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import java.util.Map;
public class DomainAuthenticationFilter extends FormAuthenticationFilter {
private Map<LoginDomain, String> loginUrlMap;
@Override
public String getLoginUrl() {
LoginDomain domain = WxbStatic.getLoginDomain();
if (domain == null || loginUrlMap.get(domain) == null) {
return super.getLoginUrl();
} else {
return loginUrlMap.get(domain);
}
}
public Map<LoginDomain, String> getLoginUrlMap() {
return loginUrlMap;
}
public void setLoginUrlMap(Map<LoginDomain, String> loginUrlMap) {
this.loginUrlMap = loginUrlMap;
}
}
复制代码
该接口作为第一层过滤未登录用户的filter
public enum DefaultFilter {
anon(AnonymousFilter.class),
authc(FormAuthenticationFilter.class),
authcBasic(BasicHttpAuthenticationFilter.class),
logout(LogoutFilter.class),
noSessionCreation(NoSessionCreationFilter.class),
perms(PermissionsAuthorizationFilter.class),
port(PortFilter.class),
rest(HttpMethodPermissionFilter.class),
roles(RolesAuthorizationFilter.class),
ssl(SslFilter.class),
user(UserFilter.class);
}
复制代码
如上 可以看到authc,因此我们复写了之后需要覆盖默认的authc filter
shiro也很贴心的提供了可以覆盖的filters的map
比如
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="${wxb.url}"/>
<property name="successUrl" value="/kzf6/page/index/index.jsp" />
<property name="unauthorizedUrl" value="/kzf6/page/error/403.jsp" />
<property name="filters">
<map>
<entry key="kickout" value-ref="kickoutSessionControlFilter"/>
<entry key="authc" value-ref="domainAuthenticationFilter"/>
</map>
</property>
复制代码
此时authc就会被domainAuthenticationFilter 给覆盖
那么当出现用户未授权登录的时候将会根据条件返回指定的登录url(此处如果需要可以动态,比如所有用户分配二级域名,然后用户自动重定向到二级域名登录)
package com.air.tqb.utils;
import com.air.tqb.common.Channel;
import com.air.tqb.common.LoginDomain;
import sun.rmi.runtime.Log;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* Created by qixiaobo on 16/8/17.
*/
public class WxbStatic {
private static final ThreadLocal<List<String>> SQL_LIST_TL = new ThreadLocal<>();
private static final ThreadLocal<String> IP_TL = new ThreadLocal<>();
private static final ThreadLocal<String> USER_TL = new ThreadLocal<>();
private static final ThreadLocal<String> ORG_TL = new ThreadLocal<>();
private static final ThreadLocal<Set<String>> IDS_OWN_ORG_TL = new ThreadLocal<>();
private static final ThreadLocal<Set<String>> PERMISSION_IDS_OWN_ORG_TL = new ThreadLocal<>();
private static final ThreadLocal<String> DATASOURCE_ROUTING_TL = new ThreadLocal<>();
private static final ThreadLocal<String> ACTION_TL = new ThreadLocal<>();
private static final ThreadLocal<ActionType> TYPE_TL = new ThreadLocal<>();
private static final ThreadLocal<String> CUSTOMER_FOR_SUPPLIER_TL = new ThreadLocal<>();
private static final ThreadLocal<Channel> CHANNEL_TL = new ThreadLocal<>();
private static final List<ThreadLocal> THREAD_LOCAL_LIST = new ArrayList<>();
private static final ThreadLocal<Boolean> SECURITY_ENABLE_TL = new ThreadLocal<>();
private static final ThreadLocal<LoginDomain> LOGIN_DOMAIN_TL=new ThreadLocal<>();
static {
THREAD_LOCAL_LIST.add(SQL_LIST_TL);
THREAD_LOCAL_LIST.add(IP_TL);
THREAD_LOCAL_LIST.add(USER_TL);
THREAD_LOCAL_LIST.add(ORG_TL);
THREAD_LOCAL_LIST.add(IDS_OWN_ORG_TL);
THREAD_LOCAL_LIST.add(PERMISSION_IDS_OWN_ORG_TL);
THREAD_LOCAL_LIST.add(DATASOURCE_ROUTING_TL);
THREAD_LOCAL_LIST.add(ACTION_TL);
THREAD_LOCAL_LIST.add(TYPE_TL);
THREAD_LOCAL_LIST.add(CUSTOMER_FOR_SUPPLIER_TL);
THREAD_LOCAL_LIST.add(CHANNEL_TL);
THREAD_LOCAL_LIST.add(SECURITY_ENABLE_TL);
THREAD_LOCAL_LIST.add(LOGIN_DOMAIN_TL);
}
public static List<String> getSqlList() {
return SQL_LIST_TL.get();
}
public static void addSql(String sql) {
if (SQL_LIST_TL.get() == null) {
SQL_LIST_TL.set(new ArrayList<String>());
}
SQL_LIST_TL.get().add(sql);
}
public static void clearSql() {
SQL_LIST_TL.remove();
}
public static String getIp() {
return IP_TL.get();
}
public static void setIp(String ip) {
IP_TL.set(ip);
}
public static void clearIp() {
IP_TL.remove();
}
public static Set<String> getIdsOwnOrg() {
return IDS_OWN_ORG_TL.get();
}
public static void setIdsOwnOrg(Set<String> IdsOwnOrg) {
IDS_OWN_ORG_TL.set(IdsOwnOrg);
}
public static Set<String> getPermissionIdsOwnOrg() {
return PERMISSION_IDS_OWN_ORG_TL.get();
}
public static void setPermissionIdsOwnOrg(Set<String> idsOwnOrg) {
PERMISSION_IDS_OWN_ORG_TL.set(idsOwnOrg);
}
public static void clearIdsOwnOrg() {
IDS_OWN_ORG_TL.remove();
}
public static String getUser() {
return USER_TL.get();
}
public static void setUser(String user) {
USER_TL.set(user);
}
public static void clearUser() {
USER_TL.remove();
}
public static String getOrg() {
return ORG_TL.get();
}
public static void setOrg(String org) {
ORG_TL.set(org);
}
public static void clearOrg() {
ORG_TL.remove();
}
public static String getDataSourceRouting() {
return DATASOURCE_ROUTING_TL.get();
}
public static void setDataSourceRouting(String routingKey) {
DATASOURCE_ROUTING_TL.set(routingKey);
}
public static void clearDataSourceRouting() {
DATASOURCE_ROUTING_TL.remove();
}
public static String getAction() {
return ACTION_TL.get();
}
public static void setAction(String action) {
ACTION_TL.set(action);
}
public static ActionType getType() {
return TYPE_TL.get();
}
public static void setChannel(Channel channel) {
CHANNEL_TL.set(channel);
}
public static Channel getChannel() {
return CHANNEL_TL.get();
}
public static void setType(ActionType type) {
TYPE_TL.set(type);
}
public static void clearAction() {
ACTION_TL.remove();
}
public static void clearType() {
TYPE_TL.remove();
}
public static String getCustomerForSupplier() {
return CUSTOMER_FOR_SUPPLIER_TL.get();
}
public static void setCustomerForSupplier(String idCustomer) {
CUSTOMER_FOR_SUPPLIER_TL.set(idCustomer);
}
public static void clearCustomerForSupplier() {
CUSTOMER_FOR_SUPPLIER_TL.remove();
}
public static void setSecurityEnable(boolean enable) {
SECURITY_ENABLE_TL.set(enable);
}
public static void clearSecurityEnable() {
SECURITY_ENABLE_TL.remove();
}
public static Boolean getSecurityEnable() {
return SECURITY_ENABLE_TL.get();
}
public static LoginDomain getLoginDomain(){
return LOGIN_DOMAIN_TL.get();
}
public static void setLoginDomain(LoginDomain domain){
LOGIN_DOMAIN_TL.set(domain);
}
public static void clearThreadLocal() {
for (ThreadLocal tl : THREAD_LOCAL_LIST) {
if (tl != null) {
tl.remove();
}
}
}
public static String getRemoteIp(HttpServletRequest request) {
String remoteAddr = request.getRemoteAddr();
String x;
if ((x = request.getHeader("X-Forwarded-For")) != null) {
remoteAddr = x;
int idx = remoteAddr.indexOf(',');
if (idx > -1) {
remoteAddr = remoteAddr.substring(0, idx);
}
}
return remoteAddr;
}
public enum ActionType {
WEB, RMI, DUBBO
}
}
复制代码
public class SpringMvcInterceptor extends HandlerInterceptorAdapter {
@Autowired
private OrgGroupService orgGroupService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
String remoteIp = getRemoteIp(request);
WxbStatic.setIp(remoteIp);
HttpSession session = request.getSession();
TbUser qUser = (TbUser) session.getAttribute("loginUser");
WxbStatic.setType(WxbStatic.ActionType.WEB);
WxbStatic.setAction(request.getRequestURI());
if (qUser != null) {
WxbStatic.setUser(qUser.getPkId());
TbOrganization currentOrganization = (TbOrganization) session.getAttribute("organization");
if (currentOrganization == null){
WxbStatic.setOrg(qUser.getIdOwnOrg());
}
else {
WxbStatic.setOrg(currentOrganization.getPkId());
WxbStatic.setCustomerForSupplier(currentOrganization.getIdCustomerCarzone());
}
WxbStatic.setIdsOwnOrg(cloneSet(orgGroupService.getTotalOrgIds(qUser.getIdOwnOrg())));
List<TbOrganization> organizationList = (List<TbOrganization>) session.getAttribute("organizationList");
Set<String> orgStrings = Sets.newHashSetWithExpectedSize(organizationList.size());
for (TbOrganization organization : organizationList){
orgStrings.add(organization.getPkId());
}
WxbStatic.setPermissionIdsOwnOrg(orgStrings);
}
if (request.getServerName().toLowerCase().contains("***")) {
WxbStatic.setLoginDomain(LoginDomain.XXX);
} else {
WxbStatic.setLoginDomain(LoginDomain.F6);
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
super.afterCompletion(request, response, handler, ex);
WxbStatic.clearThreadLocal();
}
}
复制代码
这样可以在ThreadLocal中设置指定的登录类型,后面所有的service就无需再次parse了