目录
一、介绍
本篇文章从方面进行讲解 :
- 传统web 应用场景
- 前后端分离应用场景
二、源码分析
AbstractUserDetailsAuthenticationProvider类中authenticate⽅法在最后认证成功之后实现了记住我功能,但是查看源码得知如果开启记住我,必须进⾏相关的设置。
通过对源码的分析得出自定义的实现方式:
- 传统web方式
- 需要在所添加页面内添加一个 remember-me 的参数 值 为 true on yes 1
- 在配置类开启rememberMe 功能
- 前后端分离方式
- 自定义LoginFilter 继承 UsernamePasswordAuthenticationFilter 重写 attemptAuthentication 修改通 JSON 的方式获取值
- 自定义 RememberMeServices 的实现类 ,去重写rememberMeRequestd 方法,来修改成通过其他方式获取remember-me的值。
- 将重写后的类配置到 WebSecurityConfigurerAdapter 的继承类中
- 注意 rememberService 需要被配置两次 ,一次是在认证成功后使用的 ,一次是自动登录使用的。
三、传统web应用
通过源码分析得知必须在认证请求中加⼊参数remember-me值为"true,on,yes,1"其中任意⼀个才可以完成记住我功能,这个时候修改认证界⾯:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="UTF-8">
<title>登录页面</title>
</head>
<body>
<form action="/doLogin" method="post">
用户名:<input type="text" name="uName" /></br>
密码: <input type="password" name="pwdsss">
是否记住我 : <input type="checkbox" name="remember-me" value="true"/>
<input type="submit" value="提交">
</form>
</body>
</html>
配置中开启记住我
@Configuration(proxyBeanMethods = false)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 自定义数据源
* @return
*/
@Override
@Bean
public UserDetailsService userDetailsService(){
UserDetails userDetails = User.withUsername("zs").password("{noop}123").roles("admin").build();
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(userDetails);
return inMemoryUserDetailsManager;
}
/**
* 定义安全规则
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers("/login.html").permitAll()
.anyRequest().authenticated()
.and()
.rememberMe()// 开启记住我
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/doLogin")
.failureForwardUrl("/login.html")
.usernameParameter("uName")
.passwordParameter("pwdsss")
.and()
.csrf()
.disable();
}
}
四、前后端分离应用
4.1 自定义认证类 LoginFilter
package com.bjpowenrode.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
import static org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY;
/**
* @author 千城丶Y
* @className : LoginFilter
* @date : 2022/9/3 14:50
* @Description TODO 自定义前后端登录验证的Filter
*/
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
private String passwordParameter =SPRING_SECURITY_FORM_PASSWORD_KEY;
private String usernameParameter =SPRING_SECURITY_FORM_USERNAME_KEY;
private String rememberMeParameter = SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY ;
/**
* 重写获取用户信息的方法
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
try {
//2.判断是否是 json 格式请求类型
if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
// 获取JSON 流数据
ServletInputStream inputStream = request.getInputStream();
Map<String,String> map = new ObjectMapper().readValue(inputStream, Map.class);
// 获取用户名
String userName = map.get( usernameParameter);
String pwd = map.get(passwordParameter);
String rememberMe = map.get(rememberMeParameter);
if (StringUtils.isEmpty(userName)){
userName = "";
}
if (StringUtils.isEmpty(pwd)){
pwd = "";
}
if (!ObjectUtils.isEmpty(rememberMe)){
request.setAttribute(rememberMeParameter,rememberMe);
}
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
userName, pwd);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}else {
throw new AuthenticationServiceException("认证参数格式不正确!");
}
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
@Override
public void setPasswordParameter(String passwordParameter) {
this.passwordParameter = passwordParameter;
}
@Override
public void setUsernameParameter(String usernameParameter) {
this.usernameParameter = usernameParameter;
}
public void setRememberMeParameter(String rememberMeParameter) {
this.rememberMeParameter = rememberMeParameter;
}
}
4.2 ⾃定义 RememberMeService
package com.bjpowenrode.service;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import javax.servlet.http.HttpServletRequest;
/**
* @author 千城丶Y
* @className : My
* @date : 2022/9/3 15:20
* @Description TODO 自定义 RememberMeService
*/
public class MyPersistentTokenBasedRememberMeServices extends PersistentTokenBasedRememberMeServices {
private boolean alwaysRemember;
public MyPersistentTokenBasedRememberMeServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {
super(key, userDetailsService, tokenRepository);
}
@Override
protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
if (alwaysRemember) {
return true;
}
// 这里修改为从 请求作用域中获取
String paramValue = request.getAttribute(parameter).toString();
if (paramValue != null) {
if (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on")
|| paramValue.equalsIgnoreCase("yes") || paramValue.equals("1")) {
return true;
}
}
if (logger.isDebugEnabled()) {
logger.debug("Did not send remember-me cookie (principal did not set parameter '"
+ parameter + "')");
}
return false;
}
public boolean isAlwaysRemember() {
return alwaysRemember;
}
@Override
public void setAlwaysRemember(boolean alwaysRemember) {
this.alwaysRemember = alwaysRemember;
}
}
4.3 配置应用
package com.bjpowenrode.config;
import com.bjpowenrode.filter.LoginFilter;
import com.bjpowenrode.service.MyPersistentTokenBasedRememberMeServices;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.InMemoryTokenRepositoryImpl;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* @author 千城丶Y
* @className : WebSecurityConfig
* @date : 2022/9/3 14:47
* @Description TODO 安全认证类
*/
@Configuration(proxyBeanMethods = true) // 这里如果是 false 则每次使用bean 时都会新建
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 自定义数据源
*/
@Override
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("zs").password("{noop}123").roles("admin").build());
return inMemoryUserDetailsManager;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
/**
* 暴露本地AuthenticationManagerBuilder
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 导入自定义Filter
*/
@Bean
public LoginFilter loginFilter () throws Exception {
LoginFilter loginFilter = new LoginFilter();
loginFilter.setUsernameParameter("userName");
loginFilter.setPasswordParameter("pwd");
loginFilter.setFilterProcessesUrl("/doLogin");
loginFilter.setAuthenticationManager(authenticationManagerBean());
loginFilter.setRememberMeServices(rememberMeServices()); // 设置 认证成功时使用自定义rememberMeService 认证成功后 生成cookie 的
loginFilter.setAuthenticationSuccessHandler(
(request,response,authentication)->{
Map<String,Object> result = new HashMap<>(3);
result.put("msg","登录成功!");
result.put("code",200);
result.put("authen",authentication);
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(result);
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().println(json);
}
);
loginFilter.setAuthenticationFailureHandler((request,response,authentication)->{
Map<String,Object> result = new HashMap<>(3);
result.put("msg","登录失败!");
result.put("code",500);
result.put("authen",authentication);
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(result);
response.setStatus(HttpStatus.NO_CONTENT.value());
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().println(json);
});
return loginFilter ;
}
/**
* 自定义安全
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers("/doLogin").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint((request,response,exception)->{
Map<String,Object> result = new HashMap<>(3);
result.put("msg","未登录!");
result.put("code",401);
result.put("error",exception.getMessage());
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(result);
response.setStatus(HttpStatus.EXPECTATION_FAILED.value());
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().println(json);
})
.and()
.formLogin()
.usernameParameter("userName")
.passwordParameter("pwd")
.loginProcessingUrl("/doLogin")
.successHandler((request,response,authentication)->{
Map<String,Object> result = new HashMap<>(3);
result.put("msg","登录成功!");
result.put("code",200);
result.put("authen",authentication);
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(result);
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().println(json);
})
.failureHandler((request,response,authentication)->{
Map<String,Object> result = new HashMap<>(3);
result.put("msg","登录失败!");
result.put("code",500);
result.put("authen",authentication);
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(result);
response.setStatus(HttpStatus.NO_CONTENT.value());
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().println(json);
})
.and()
.rememberMe()
.rememberMeServices(rememberMeServices()) // 设置自动登录时使用rememberService
.and()
.logout()
.logoutRequestMatcher(new OrRequestMatcher(
new AntPathRequestMatcher("/logout", HttpMethod.GET.name()),
new AntPathRequestMatcher("/logout",HttpMethod.POST.name())
))
.logoutSuccessHandler((res,resp,authentication)->{
Map<String,Object> rs = new HashMap<>();
rs.put("msg","退出登录成功");
rs.put("用户信息",authentication);
resp.setStatus(HttpStatus.OK.value());
String json = new ObjectMapper().writeValueAsString(rs);
resp.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
resp.getWriter().println(json);
})
.and()
.csrf()
.disable();
// 替换掉这个Filter
http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}
/**
* 引入自定义RememberMeService
*/
@Bean
public RememberMeServices rememberMeServices(){
// 这个 如果是用内存的话不能new 需要让他是对象 才可以,不然数据会不一致
MyPersistentTokenBasedRememberMeServices myPersistentTokenBasedRememberMeServices = new MyPersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(),userDetailsService(),new InMemoryTokenRepositoryImpl());
return myPersistentTokenBasedRememberMeServices;
}
}