需要明确:
已正确配置跨域资源共享(CORS)(不然也不可能看到登录成功了)。
已正确配置了跨站请求伪造(CSRF)(登录时需要携带 _csrf.token)。
全部使用 POST 提交的数据。
本文相关知识:
MD5 加密与Spring Security 加密介绍。
- 注册时,使用 post 注册,已成功。
- 登录时,已经登录成功,效果如下:
- 前台查看axios响应:
说明:
登录成功与失败,都是响应 JSON 数据。
这里登录成功后,进行了重定向,导致无 Access-Control-Allow-Origin。
附上登录失败的响应:
注:全部使用 POST:
问题说明
出现这种情况,毕竟是少数(其他请求GET/POST等全都正常,目前只发现 Spring Boot Security 登录成功后重定向时出现这种问题)。
响应 JSON 数据解决办法有两种思路:
- 登录成功后进行重定向时,携带 Access-Control-Allow-Origin。
- 登录成功后,不进行重定向,直接响应结果(我这里登录结果全部响应的是JSON数据)。
from 提交数据进行跳转:
- 跳转时在响应中添加 Header 即可。
解决方案:
在 Spring Boot Security 中配置登录成功后,进行自定义处理,接口为:
package org.springframework.security.web.authentication;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
/**
* Strategy used to handle a successful user authentication.
* <p>
* Implementations can do whatever they want but typical behaviour would be to control the
* navigation to the subsequent destination (using a redirect or a forward). For example,
* after a user has logged in by submitting a login form, the application needs to decide
* where they should be redirected to afterwards (see
* {@link AbstractAuthenticationProcessingFilter} and subclasses). Other logic may also be
* included if required.
*
* @author Luke Taylor
* @since 3.0
*/
public interface AuthenticationSuccessHandler {
/**
* Called when a user has been successfully authenticated.
*
* @param request the request which caused the successful authentication
* @param response the response
* @param authentication the <tt>Authentication</tt> object which was created during
* the authentication process.
*/
void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws IOException, ServletException;
}
通过实现该接口的 onAuthenticationSuccess 方法进行操作。
解决方案一
直接响应 JSON 数据:
代码说明:
在请求(HttpServletRequest request)中获取你需要的Header。
package cn.com.xuxiaowei.passport.handler;
import com.alibaba.fastjson.JSON;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* 用于处理成功用户身份验证的策略。
*
* @author xuxiaowei
* @since 0.0.1
*/
public class LoginAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Map<String, Object> map = new HashMap<>(4);
Map<String, Object> data = new HashMap<>(4);
map.put("data", data);
map.put("code", 0);
map.put("msg", "登录成功");
String accessControlAllowOrigin = request.getHeader("Access-Control-Allow-Origin");
response.addHeader("Access-Control-Allow-Origin", accessControlAllowOrigin);
response.addHeader("Access-Control-Allow-Credentials", "true");
response.setContentType("text/json;charset=UTF-8");
response.getWriter().println(JSON.toJSON(map));
response.setStatus(HttpServletResponse.SC_OK);
response.flushBuffer();
}
}
在 WebSecurityConfigurerAdapter 中配置:
两种方案都需要此配置:
@Override
protected void configure(HttpSecurity http) throws Exception {
// 其他代码省略
// 授权登录成功后处理
http.formLogin().successHandler(loginAuthenticationSuccessHandler());
}
@Bean
LoginAuthenticationSuccessHandler loginAuthenticationSuccessHandler() {
return new LoginAuthenticationSuccessHandler();
}
响应结果:
解决方案二
package cn.com.xuxiaowei.passport.handler;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 用于处理成功用户身份验证的策略。
*
* @author xuxiaowei
* @since 0.0.1
*/
public class LoginAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
/**
* 注意:
* 不可设置:response.addHeader("Access-Control-Allow-Credentials", "true");
* 否则响应中的的 Access-Control-Allow-Credentials 为:true,true
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
String accessControlAllowOrigin = request.getHeader("Access-Control-Allow-Origin");
response.addHeader("Access-Control-Allow-Origin", accessControlAllowOrigin);
request.getRequestDispatcher("/login/success.do").forward(request, response);
}
}
注意:
不可设置:response.addHeader("Access-Control-Allow-Credentials", "true");
否则响应中的的 Access-Control-Allow-Credentials 为:true,true;
因为已经存在了 Access-Control-Allow-Credentials 为:true。
虽然有警告,但是这是时数据正常响应的:
不设置 Access-Control-Allow-Credentials 就对了。
在 WebSecurityConfigurerAdapter 中配置同方案一。
效果相同。
解决方案三
如果你使用的是from提交数据进行跳转的话:
package cn.com.xuxiaowei.passport.handler;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 用于处理成功用户身份验证的策略。
*
* @author xuxiaowei
* @since 0.0.1
*/
public class LoginAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
String accessControlAllowOrigin = request.getHeader("Access-Control-Allow-Origin");
response.addHeader("Access-Control-Allow-Origin", accessControlAllowOrigin);
response.addHeader("Access-Control-Allow-Credentials", "true");
super.onAuthenticationSuccess(request, response, authentication);
}
}
相关知识补充
Spring Security 密码加密
/**
* Spring Security 密码加密相关测试
*
* @author xuxiaowei
* @since 0.0.1
*/
public class SpringSecurityTests {
/**
* 用来测试的密码
*/
private final String password = "https://xuxiaowei.blog.csdn.net";
@Test
public void passwordEncoder() {
// 密码编辑器
PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
System.err.println("加密前的密码:" + password);
String passwordPncode1 = passwordEncoder.encode(password);
String passwordPncode2 = passwordEncoder.encode(password);
System.err.println("第一次加密:" + passwordPncode1);
System.err.println("第二次加密:" + passwordPncode2);
boolean matches1 = passwordEncoder.matches(password, passwordPncode1);
boolean matches2 = passwordEncoder.matches(password, passwordPncode2);
System.err.println("第一次加密密码比较:" + matches1);
System.err.println("第二次加密密码比较:" + matches2);
}
}
测试结果:
加密前的密码:https://xuxiaowei.blog.csdn.net
第一次加密:{bcrypt}$2a$10$z.E5k6q6MHG9iVN/zRM0FOknS748k5b8gvGp/8njgLjDFZ9GyvcVK
第二次加密:{bcrypt}$2a$10$yosn9iar2ARMikmxLFDy6uYHyEu02DD8NOh/nMgOZJcjKQnWV9EdW
第一次加密密码比较:true
第二次加密密码比较:true
说明:
每次加密,即使是相同的密码,加密后的结果也不会相同。
MD5密码加密
import org.junit.Test;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* MD5 密码解密测试
*
* @author xuxiaowei
* @since 0.0.1
*/
public class Md5EncodeTests {
/**
* 用来测试的密码
*/
private final String password = "123456";
@Test
public void md5() {
System.err.println("加密前的密码:" + password);
String passwordEncode1 = md5Encode(password);
String passwordEncode2 = md5Encode(password);
System.err.println("第一次加密:" + passwordEncode1);
System.err.println("第二次加密:" + passwordEncode2);
boolean equals1 = passwordEncode1.equals(md5Encode(password));
boolean equals2 = passwordEncode2.equals(md5Encode(password));
System.err.println("第一次加密密码比较:" + equals1);
System.err.println("第二次加密密码比较:" + equals2);
}
/**
* MD5 加密
*
* @param text 需要加密的字符串
* @return 返回 MD5 加密后的字符串
*/
private String md5Encode(String text) {
try {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
messageDigest.update(text.getBytes());
byte[] bytes = messageDigest.digest();
int i;
StringBuilder buf = new StringBuilder();
for (byte b : bytes) {
i = b;
i = i < 0 ? i + 256 : i;
if (i < 16) {
buf.append("0");
}
buf.append(Integer.toHexString(i));
}
return buf.toString();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
}
测试结果:
加密前的密码:123456
第一次加密:e10adc3949ba59abbe56e057f20f883e
第二次加密:e10adc3949ba59abbe56e057f20f883e
第一次加密密码比较:true
第二次加密密码比较:true
说明:
MD5加密,针对同一密码(资源)加密结果每次运行都是一样的。所以出于安全考虑,密码加密不推荐使用 MD5,安全系数低,有泄漏的风险。
若你使用的是简易密码,如:123456,或者两人使用相同的密码,当密码保管不慎泄漏时,后果很严重。
更不要在多个账户中使用同一个密码。