一.项目配置
二.RememberMe的作用
1.默认可以所有接口访问,需要在securityConfig里配置securityFilterChain
2.加入了.remember()就是默认加入了rememberMe过滤器链rememberFilterChain,一共有35个过滤器链,不配置默认14个启用
三.securityConfig里配置securityFilterChain增加配置rememberFilterChain
package com.huang.springsecurity.comments.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.huang.springsecurity.comments.result.RespBean;
import com.huang.springsecurity.model.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.*;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.*;
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class SecurityConfig {
@Bean
PasswordEncoder passwordEncoder() {
//这个表示使用明文密码
// return NoOpPasswordEncoder.getInstance();
//表示使用 bcrypt 做密码加密
// return new BCryptPasswordEncoder();
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap();
encoders.put(encodingId, new BCryptPasswordEncoder(12));
encoders.put("ldap", new LdapShaPasswordEncoder());
encoders.put("MD4", new Md4PasswordEncoder());
encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new StandardPasswordEncoder());
encoders.put("argon2", new Argon2PasswordEncoder());
return new DelegatingPasswordEncoder(encodingId, encoders);
}
/**
* 给登录页面放行
* Spring Security 给一个地址放行,有两种方式:
* 1. 被放行的资源,不需要经过 Spring Security 过滤器链(静态资源一般使用这种)。
* 2. 经过 Spring Security 过滤器链,但是不拦截(如果是一个接口想要匿名访问,一般使用这种)。
* <p>
* 下面这种方形方式是第一种
*
* @return
*/
@Bean
WebSecurityCustomizer securityCustomizer() {
return new WebSecurityCustomizer() {
@Override
public void customize(WebSecurity web) {
web.ignoring().antMatchers("/login.html");
}
};
}
/**
* 自己手动配置安全过滤器链
*
* @return
*/
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
//开始认证
http.authorizeRequests()
//请求路径如果是 /login.html,则这个请求可以匿名通过
.antMatchers("/login.html").anonymous()
//所有的请求,类似于 shiro 中的 /**
.anyRequest()
//必须要认证之后才能访问,类似于 shiro 中的 authc
.authenticated()
.and()
//开始配置登录表单
.formLogin()
//配置登录页面,如果访问了一个需要认证之后才能访问的页面,那么就会自动跳转到这个页面上来
.loginPage("/login.html")
//配置处理登录请求的接口,本质上其实就是配置过滤器的拦截规则,将来的登录请求就会在过滤器中被处理
.loginProcessingUrl("/doLogin")
//配置登录表单中用户名的 key
.usernameParameter("username")
//配置登录表单中用户密码
.passwordParameter("password")
//配置登录成功后的跳转地址
// .defaultSuccessUrl("/hello")
// .failureUrl("/login.html")
//登录成功处理器
//req:当前请求对象
//resp:当前响应对象
//auth:当前认证成功的用户信息
.successHandler((req, resp, auth) -> {
resp.setContentType("application/json;charset=utf-8");
User principal = (User) auth.getPrincipal();
principal.setPassword(null);
RespBean respBean = RespBean.ok("登录成功", principal);
String s = new ObjectMapper().writeValueAsString(respBean);
resp.getWriter().write(s);
})
//登录失败的回调
.failureHandler((req, resp, e) -> {
resp.setContentType("application/json;charset=utf-8");
//登录失败可能会有多种原因
RespBean respBean = RespBean.error("登录失败");
if (e instanceof BadCredentialsException) {
respBean.setMsg("用户名或者密码输入错误,登录失败");
} else if (e instanceof UsernameNotFoundException) {
//默认情况下,这个分支是不会进来的,Spring Security 自动隐藏了了这个异常,如果系统中发生了 UsernameNotFoundException 会被自动转为 BadCredentialsException 异常然后抛出来
} else if (e instanceof LockedException) {
//如果 com.qfedu.security02.model.User.isAccountNonLocked 方法返回 false,就会进入到这里来
respBean.setMsg("账户被锁定,登录失败");
} else if (e instanceof AccountExpiredException) {
//com.qfedu.security02.model.User.isAccountNonExpired
respBean.setMsg("账户过期,登录失败");
} else if (e instanceof CredentialsExpiredException) {
respBean.setMsg("密码过期,登录失败");
} else if (e instanceof DisabledException) {
respBean.setMsg("账户被禁用,登录失败");
}
ObjectMapper om = new ObjectMapper();
String s = om.writeValueAsString(respBean);
PrintWriter out = resp.getWriter();
out.write(s);
})
.and()
//默认情况下,开启了 RememberMe 之后,所有的接口都可以通过 RememberMe 登录之后访问
//本质上,这个方法就是向 Spring Security 过滤器链中添加了一个过滤器 RememberMeAuthenticationFilter
.rememberMe()
.key("huang")
.and()
//关闭 csrf 防御机制,这个 disable 方法本质上就是从 Spring Security 的过滤器链上移除掉 csrf 过滤器
.csrf().disable()
.exceptionHandling()
//如果用户未登录就访问某一个页面,就会触发当前方法
.authenticationEntryPoint((req, resp, authException) -> {
resp.setContentType("application/json;charset=utf-8");
RespBean respBean = RespBean.error("尚未登录,请登录");
String s = new ObjectMapper().writeValueAsString(respBean);
resp.getWriter().write(s);
});
return http.build();
}
}
增加的RememberMe配置为
.and()
//默认情况下,开启了 RememberMe
//本质上,这个方法就是向 Spring Security 过滤器链中添加了一个过滤器 RememberMeAuthenticationFilter
.rememberMe()
.key("huang")
四. RememberMe的登录返回值
开启 RememberMe 之后,登录成功后,服务端会响应一个 rm 字符串回来:emhhbmdzYW46MTY1OTQwMzM3NzE0ODowNjczYzdlYWFlNjE5MWU1YzY1MDYyOWRhMWUwZWQ5Zg
,这是一个 base64 编码之后的字符串,解码之后,分为三部分:
- 用户名
- 时间戳
- 加密的字符串(根据用户名+用户密码+时间戳+key加密生成的字符串,不可解密)
以后每次请求的时候,都会自动携带上这个 Cookie,服务端收到 Cookie 之后,会解析出来用户名和时间戳,通过时间戳就能判断出 Cookie 是否已经过期,没有过期的话,根据用户名查询出用户密码,然后根据和用户名、用户密码、时间戳、key 进行加密,将加密后的字符串跟 Cookie 中的第三部分进行比较。