spring security 兼容表单和json请求+跨域+r
image.png
跨域配置
http.addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class);
跨域filter类
@Component
public class CorsFilter implements Filter {
private final static Logger log = LoggerFactory.getLogger(CorsFilter.class);
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//servletRequest.setCharacterEncoding("UTF-8");
HttpServletResponse res = (HttpServletResponse) servletResponse;
HttpServletRequest request = (HttpServletRequest) servletRequest;
Map<String, String[]> parameterMap = request.getParameterMap();
log.info(request.getMethod() + ">>>>>>>>>>>>>>>" + JSONObject.toJSONString(parameterMap));
String origin = request.getHeader("Origin");
log.info("请求origin:" + origin);
res.setHeader("Access-Control-Allow-Origin", "*");
res.setContentType("text/html;charset=UTF-8");
res.setHeader("Access-Control-Allow-Methods", "*");
res.setHeader("Access-Control-Max-Age", "86400");
res.setHeader("Access-Control-Allow-Headers", "Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With,userId,token");
res.setHeader("Access-Control-Allow-Credentials", "true");
res.setHeader("XDomainRequestAllowed", "1");
filterChain.doFilter(servletRequest, servletResponse);
}
}
自定义UserAuthenticationFilter 继承 UsernamePasswordAuthenticationFilter 替换掉原来的 UsernamePasswordAuthenticationFilter
http.addFilterAt(UserAuthenticationFilterBean(), UsernamePasswordAuthenticationFilter.class);
UserAuthenticationFilter 类
public class UserAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private ThreadLocal<Map<String,String>> threadLocal = new ThreadLocal<>();
@Override
protected String obtainPassword(HttpServletRequest request) {
String password = this.getBodyParams(request).get(super.getPasswordParameter());
if(!StringUtils.isEmpty(password)){
return password;
}
return super.obtainPassword(request);
}
@Override
protected String obtainUsername(HttpServletRequest request) {
String username = this.getBodyParams(request).get(super.getUsernameParameter());
if(!StringUtils.isEmpty(username)){
return username;
}
return super.obtainUsername(request);
}
/**
* 获取body参数 body中的参数只能获取一次
* @param request
* @return
*/
private Map<String,String> getBodyParams(HttpServletRequest request){
Map<String,String> bodyParams = threadLocal.get();
if(bodyParams==null) {
ObjectMapper objectMapper = new ObjectMapper();
try (InputStream is = request.getInputStream()) {
bodyParams = objectMapper.readValue(is, Map.class);
} catch (IOException e) {
}
if(bodyParams==null) bodyParams = new HashMap<>();
threadLocal.set(bodyParams);
}
return bodyParams;
}
}
创建UserAuthenticationFilter
private UserAuthenticationFilter UserAuthenticationFilterBean() throws Exception {
UserAuthenticationFilter userAuthenticationFilter = new UserAuthenticationFilter();
userAuthenticationFilter.setAuthenticationManager(super.authenticationManager());
userAuthenticationFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
userAuthenticationFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());
userAuthenticationFilter.setUsernameParameter("username");
userAuthenticationFilter.setPasswordParameter("password");
userAuthenticationFilter.setRememberMeServices(rememberMeServices());
return userAuthenticationFilter;
}
到这里遇到一个坑
后面想要加入rememberme 功能 按照正常的配置方法是不行的
因为UsernamePasswordAuthenticationFilter已经被我们替换掉了
所以只能自己去配置userAuthenticationFilter的RememberMeServices
创建remembermeservice
@Bean
public RememberMeServices rememberMeServices() {
PersistentTokenBasedRememberMeServices rememberMeServices = new PersistentTokenBasedRememberMeServices(REMEMBER_ME, myUserDetailsService, myRedisTokenRepository);
rememberMeServices.setParameter(REMEMBER_ME);
rememberMeServices.setTokenValiditySeconds(3600);
return rememberMeServices;
}
redis存储实现类
@Component
public class MyRedisTokenRepository implements PersistentTokenRepository {
private final static long TOKEN_VALID_DAYS = 30;
private final static Logger log = LoggerFactory.getLogger(MyRedisTokenRepository.class);
@Autowired
private RedisTemplate redisTemplate;
@Override
public void createNewToken(PersistentRememberMeToken token) {
if (log.isDebugEnabled()) {
log.debug("token create seriesId: [{}]", token.getSeries());
}
String key = generateKey(token.getSeries());
HashMap<String, String> map = new HashMap();
map.put("username", token.getUsername());
map.put("tokenValue", token.getTokenValue());
map.put("date", String.valueOf(token.getDate().getTime()));
redisTemplate.opsForHash().putAll(key, map);
redisTemplate.expire(key, TOKEN_VALID_DAYS, TimeUnit.DAYS);
}
@Override
public void updateToken(String series, String tokenValue, Date lastUsed) {
String key = generateKey(series);
HashMap<String, String> map = new HashMap();
map.put("tokenValue", tokenValue);
map.put("date", String.valueOf(lastUsed.getTime()));
redisTemplate.opsForHash().putAll(key, map);
redisTemplate.expire(key, TOKEN_VALID_DAYS, TimeUnit.DAYS);
}
@Override
public PersistentRememberMeToken getTokenForSeries(String seriesId) {
String key = generateKey(seriesId);
List<String> hashKeys = new ArrayList<>();
hashKeys.add("username");
hashKeys.add("tokenValue");
hashKeys.add("date");
List<String> hashValues = redisTemplate.opsForHash().multiGet(key, hashKeys);
String username = hashValues.get(0);
String tokenValue = hashValues.get(1);
String date = hashValues.get(2);
if (null == username || null == tokenValue || null == date) {
return null;
}
Long timestamp = Long.valueOf(date);
Date time = new Date(timestamp);
PersistentRememberMeToken token = new PersistentRememberMeToken(username, seriesId, tokenValue, time);
return token;
}
@Override
public void removeUserTokens(String username) {
if (log.isDebugEnabled()) {
log.debug("token remove username: [{}]", username);
}
byte[] hashKey = redisTemplate.getHashKeySerializer().serialize("username");
RedisConnection redisConnection = redisTemplate.getConnectionFactory().getConnection();
try (Cursor<byte[]> cursor = redisConnection.scan(ScanOptions.scanOptions().match(generateKey("*")).count(1024).build())) {
while (cursor.hasNext()) {
byte[] key = cursor.next();
byte[] hashValue = redisConnection.hGet(key, hashKey);
String storeName = (String) redisTemplate.getHashValueSerializer().deserialize(hashValue);
if (username.equals(storeName)) {
redisConnection.expire(key, 0L);
return;
}
}
} catch (IOException ex) {
log.warn("token remove exception", ex);
}
}
/**
* 生成key
*
* @param series
* @return
*/
private String generateKey(String series) {
return "spring:security:rememberMe:token:" + series;
}
}
到这里还是无法成功实现rememberme
继续debug
发现验证cookie的时候使用的是
TokenBasedRememberMeServices
由于我们想要保存redis 在UserAuthenticationFilter中使用的是
PersistentTokenBasedRememberMeServices
生成cookie和验证的方法不一致
还需要加上配置
http
.rememberMe()
.rememberMeServices(rememberMeServices());
现在表单登录可以正常使用rememberme了,
但是application/json类型的登录请求还是没有返回rememberme 的cookie
继续debug
AbstractRememberMeServices中的 rememberMeRequested 方法使用
request.getParameter(parameter); 获取的是表单参数
现在只能重写这个方法了
MyPersistentTokenBasedRememberMeServices
public class MyPersistentTokenBasedRememberMeServices extends PersistentTokenBasedRememberMeServices {
public MyPersistentTokenBasedRememberMeServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {
super(key, userDetailsService, tokenRepository);
}
@Override
protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
boolean b = super.rememberMeRequested(request, parameter);
if (!b) {
String rememberMe = ApplicationJsonContextHolder.getString(parameter);
if (rememberMe != null &&
(rememberMe.equalsIgnoreCase("true") ||
rememberMe.equalsIgnoreCase("on") ||
rememberMe.equalsIgnoreCase("yes") ||
rememberMe.equals("1"))) {
return true;
}
}
return b;
}
}
把配置类中的remembermeservice替换了
@Bean
public RememberMeServices rememberMeServices() {
MyPersistentTokenBasedRememberMeServices rememberMeServices = new MyPersistentTokenBasedRememberMeServices(REMEMBER_ME, myUserDetailsService, myRedisTokenRepository);
rememberMeServices.setParameter(REMEMBER_ME);
rememberMeServices.setTokenValiditySeconds(3600);
return rememberMeServices;
}
因为要解决application/json复用的问题,
新建了一个ApplicationJsonContextHolder类 ,
ApplicationJsonContextHolder
public class ApplicationJsonContextHolder {
private final static Logger log = LoggerFactory.getLogger(ApplicationJsonContextHolder.class);
private static ThreadLocal<Map<String, String>> threadLocal = new ThreadLocal<>();
public static void reset() {
threadLocal.remove();
}
/**
* 获取body参数 body中的参数只能获取一次
*
* @param request
* @return
*/
public static void init(HttpServletRequest request) {
Map<String, String> bodyParams = threadLocal.get();
if (bodyParams == null) {
ObjectMapper objectMapper = new ObjectMapper();
try (InputStream is = request.getInputStream()) {
bodyParams = objectMapper.readValue(is, Map.class);
} catch (IOException e) {
}
if (bodyParams == null) bodyParams = new HashMap<>();
threadLocal.set(bodyParams);
}
//log.info("json数据》》》》》》》》》》》》" + JSONObject.toJSONString(bodyParams));
}
public static Object get(String key) {
return threadLocal.get().get(key);
}
public static String getString(String key) {
return String.valueOf(get(key));
}
}
在过滤器中初始化这个ApplicationJsonContextHolder
@Component
public class ApplicationJsonFilter implements Filter {
private final static Logger log = LoggerFactory.getLogger(ApplicationJsonFilter.class);
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
ApplicationJsonContextHolder.init((HttpServletRequest) servletRequest);
filterChain.doFilter(servletRequest, servletResponse);
/*用完要删掉*/
ApplicationJsonContextHolder.reset();
}
}
配置过滤器
http.addFilterBefore(applicationJsonFilter, UsernamePasswordAuthenticationFilter.class);
到这里应该就能同时支持表单请求和application/json请求的rememberme功能了
用了 threadlocal request中的输入流没有内容了
最后还是使用了装饰器模式
https://www.jianshu.com/p/58c1afb8e6af