问题描述

用于请求到服务端时,SpringSecurity会对用户进行鉴权(如果需要)。如果未认证,就会重定向到登录页进行认证授权。SpringSecurity的重定向是直接获取到
客户端Ip,如果说我们使用Nginx进行转发,而且使用的内网Ip,就会重定向到内网Ip。

场景:

  • tomcat服务器一台,内网ip:192.168.0.101,端口8080
  • nginx服务器一台,外网ip:172.1.1.1,端口80

客户端使用浏览器访问 http://172.1.1.1,会重定向到http://192/168.0.101:8080/login,该ip为内网ip,肯定是访问不到的。

解决思路

1. 自定义SpringSecurity重定向逻辑

要想自定义SpringSecurity重定向逻辑,就需要了解SpringSecurity的认证流程。

ExceptionTranslationFilterdoFilter方法,我们发现这里抛出了AuthenticationException

调用AccessDeniedException方法处理异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private void handleSpringSecurityException(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, RuntimeException exception)
throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
// ......
}
else if (exception instanceof AccessDeniedException) {
// 获取认证信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 是否为匿名或者记住我?当然匿名啊,都没有登录
if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
logger.debug(
"Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point",
exception);

sendStartAuthentication(
request,
response,
chain,
new InsufficientAuthenticationException(
messages.getMessage(
"ExceptionTranslationFilter.insufficientAuthentication",
"Full authentication is required to access this resource")));
}
else {
logger.debug(
"Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
exception);

accessDeniedHandler.handle(request, response,
(AccessDeniedException) exception);
}
}
}

因为抛出的是AccessDeniedException,这里判断是否为匿名或者记住我,调用sendStartAuthentication方法

1
2
3
4
5
6
7
8
protected void sendStartAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
SecurityContextHolder.getContext().setAuthentication(null);
requestCache.saveRequest(request, response);
logger.debug("Calling Authentication entry point.");
authenticationEntryPoint.commence(request, response, reason);
}

sendStartAuthentication方法中调用authenticationEntryPoint.commence,debug到它的实现类LoginUrlAuthenticationEntryPoint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {

String redirectUrl = null;
// 是否使用 forward? 这里是重定向好吗。
if (useForward) {

if (forceHttps && "http".equals(request.getScheme())) {
// First redirect the current request to HTTPS.
// When that request is received, the forward to the login page will be
// used.
redirectUrl = buildHttpsRedirectUrlForRequest(request);
}

if (redirectUrl == null) {
String loginForm = determineUrlToUseForThisRequest(request, response,
authException);

if (logger.isDebugEnabled()) {
logger.debug("Server side forward to: " + loginForm);
}

RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);

dispatcher.forward(request, response);

return;
}
}
// 是重定向走这里
else {
// redirect to login page. Use https if forceHttps true

redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);

}

redirectStrategy.sendRedirect(request, response, redirectUrl);
}

redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);,也就是这一句构建了重定向到登录页到url

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
protected String buildRedirectUrlToLoginPage(HttpServletRequest request,
HttpServletResponse response, AuthenticationException authException) {

String loginForm = determineUrlToUseForThisRequest(request, response,
authException);

if (UrlUtils.isAbsoluteUrl(loginForm)) {
return loginForm;
}

int serverPort = portResolver.getServerPort(request);
String scheme = request.getScheme();

RedirectUrlBuilder urlBuilder = new RedirectUrlBuilder();

urlBuilder.setScheme(scheme);
urlBuilder.setServerName(request.getServerName());
urlBuilder.setPort(serverPort);
urlBuilder.setContextPath(request.getContextPath());
urlBuilder.setPathInfo(loginForm);

if (forceHttps && "http".equals(scheme)) {
Integer httpsPort = portMapper.lookupHttpsPort(Integer.valueOf(serverPort));

if (httpsPort != null) {
// Overwrite scheme and port in the redirect URL
urlBuilder.setScheme("https");
urlBuilder.setPort(httpsPort.intValue());
}
else {
logger.warn("Unable to redirect to HTTPS as no port mapping found for HTTP port "
+ serverPort);
}
}

return urlBuilder.getUrl();
}

也就是说我们重写 LoginUrlAuthenticationEntryPointbuildRedirectUrlToLoginPage的方法不就可以了吗?Ok,直接贴源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public  class UnauthorizedEntryPoint extends LoginUrlAuthenticationEntryPoint {

UnauthorizedEntryPoint() {
super(LOGIN_PAGE);
}

@Override
protected String buildRedirectUrlToLoginPage(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
String loginForm = determineUrlToUseForThisRequest(request, response, authException);
if (UrlUtils.isAbsoluteUrl(loginForm)) {
return loginForm;
}
String url = ServletUtil.getRedirectUrl(request, loginForm);
log.info("redirect url: {}", url);
HttpSession session = request.getSession();
if (session != null) {
session.setAttribute(REDIRECT_FROM_SESSION_NAME, ServletUtil.getRedirectUrl(request));
}
return url;
}
}

ServletUtil相关源码,主要用于获取真实客户端信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public class ServletUtil {


public static int getPort(HttpServletRequest request) {
String port = request.getHeader("X-Real-Port");
if (StringUtils.isEmpty(port) || "unknown".equalsIgnoreCase(port)) {
port = String.valueOf(request.getServerPort());
}
return Integer.parseInt(port);
}

public static String getIpAddr(HttpServletRequest request) {
try {
String ip = request.getHeader("X-Real-Host");
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Forwarded-For");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
String host = request.getHeader("host");
if (StringUtils.hasLength(host)) {
ip = host.split(":")[0];
}
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
if (ip.equals("127.0.0.1") || ip.equals("0:0:0:0:0:0:0:1")) {
// 根据网卡取本机配置的IP
InetAddress inetAddress = InetAddress.getLocalHost();
ip = inetAddress.getHostAddress();
}
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ip.indexOf(",") > 0) {
ip = ip.substring(0, ip.indexOf(","));
}
return ip;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

public static String getScheme(HttpServletRequest request) {
String scheme = request.getHeader("X-Forwarded-Proto");
if (StringUtils.isEmpty(scheme) || "unknown".equalsIgnoreCase(scheme)) {
scheme = String.valueOf(request.getScheme());
}
return scheme;
}

public static String getRedirectUrl(HttpServletRequest request) {
return getRedirectUrl(request, request.getServletPath());
}

public static String getRedirectUrl(HttpServletRequest request, String path) {
RedirectUrlBuilder urlBuilder = new RedirectUrlBuilder();
urlBuilder.setPathInfo(path);
urlBuilder.setScheme(getScheme(request));
urlBuilder.setServerName(getIpAddr(request));
urlBuilder.setPort(ServletUtil.getPort(request));
urlBuilder.setContextPath(request.getContextPath());
urlBuilder.setQuery(request.getQueryString());
return urlBuilder.getUrl();
}
}

当然了,还需要在WebSecurityConfigurerAdapter进行配置

1
2
3
4
5
6
7
8
9
10
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.exceptionHandling().authenticationEntryPoint(new UnauthorizedEntryPoint()).and()
// ... 此处省略其他配置
}
}

2. 将真实的客户端信息传递给服务端

那这些真实客户端信息到底怎么来的,必须nginx转发的啊。

配置 nginx

1
2
3
4
5
6
7
8
9
10
11
12
13
location / {
proxy_pass http://192.168.0.101:8080;
proxy_redirect default;
proxy_set_header X-Real-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $server_port;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_connect_timeout 1;
proxy_send_timeout 30;
proxy_read_timeout 60;
client_max_body_size 50m;
}

这下就可以重定向到真实到客户端地址了。当然你可能会说,干嘛不做转发。转发是可以的,考虑项目使用的是相对地址,使用重定向也可以规避很多问题。绝对地址的话需要考虑到contextPath