前言

前面的章节现已介绍了,咱们如何quick start一个spring security,然后还有做了一些图片验证之类的功用。还有从数据库中获取咱们用户的信息。研究了spring security整个认证的流程。

本章呢,首要解说如何记载咱们的认证信息在下次会话被封闭之后还能够持续运用,而不必重复登录。

本章内容

  1. remember-me主动登录怎样玩?
  2. Remember me的源码剖析
  3. 耐久化remember me
  4. 二次校验功用
  5. remember-me前后端别离

为什么有remember-me?

小黑: 小白啊, 问一个问题,假如让你自己完成remember me功用,你应该怎样完成呢?

小白: 为什么要完成? 直接给cookie设置过期时刻不就好了? 让浏览器jessionID保存到电脑上完事, 何须再去完成一个劳什子remember-me? 你这不是给咱们后端人员添加工作量么? 你不清楚咱们劳动人民的辛苦么? 你….

1

是啊? 为什么 spring security还要自己完成一个 remember-me 呢? 他有什么考量的当地是咱们不知道的呢?

  1. 不安全. cookie有好多安全问题, 不安全(即便运用了spring security的remember-me也仍是会有一点不安全, 但比没有好多了)
  2. session不可控. 咱们无法感知session怎样样
  3. 接入spring security后会有更多的功用

quick start

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RememberApplication {
	public static void main(String[] args) throws Exception {
		SpringApplication.run(RememberApplication.class, args);
	}
}
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class IndexController {
	@GetMapping("hello")
	public String hello() {
		return "hello";
	}
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
	@Bean
	public PasswordEncoder passwordEncoder() {
		return PasswordEncoderFactories.createDelegatingPasswordEncoder();
	}
	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
		return httpSecurity
				.authorizeRequests()
				.anyRequest()
				.authenticated()
				.and()
				.formLogin()
				.permitAll()
				.and()
				.rememberMe()
				.key("zhazha") // 这儿有点像撒盐
				.and()
				.csrf()
				.disable()
				.build();
	}
}

image-20221126104309912

image-20221126111717047

底层都做了什么?

要了解remember-me底层都做了什么十分简略, 只需重视RememberMeServices接口就好

public interface RememberMeServices {
   Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);
   void loginFail(HttpServletRequest request, HttpServletResponse response);
   void loginSuccess(HttpServletRequest request, HttpServletResponse response,
         Authentication successfulAuthentication);
}

image-20230108233441391

小黑: “咱们的quick start 就是走的最底下的那个类”

小白: “那咱们的remember-me功用在哪调用的呢?”

小黑: “咱们中心认证流程的终究一步骤是保存用户登录信息, 这也是咱们remember-me功用运用的位置”

image-20230108233940432

// AbstractRememberMeServices
@Override
public final void loginSuccess(HttpServletRequest request, HttpServletResponse response,
      Authentication successfulAuthentication) {
   if (!rememberMeRequested(request, this.parameter)) {
      this.logger.debug("Remember-me login not requested.");
      return;
   }
   onLoginSuccess(request, response, successfulAuthentication);
}

小黑: “首先会判别你是否敞开了remember-me功用, 然后在调用真正的记住我功用”

// TokenBasedRememberMeServices
@Override
public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
      Authentication successfulAuthentication) {
   String username = retrieveUserName(successfulAuthentication);
   String password = retrievePassword(successfulAuthentication);
   if (!StringUtils.hasLength(password)) {
      UserDetails user = getUserDetailsService().loadUserByUsername(username);
      password = user.getPassword();
   }
    // 默许回来两周时刻
   int tokenLifetime = calculateLoginLifetime(request, successfulAuthentication);
    // 拿到当时时刻
   long expiryTime = System.currentTimeMillis();
    // 默许过期时刻核算是当时时刻之后的两周时刻
   expiryTime += 1000L * ((tokenLifetime < 0) ? TWO_WEEKS_S : tokenLifetime);
    // 下面函数中的中心代码: String data = username + ":" + tokenExpiryTime + ":" + password + ":" + getKey();
    // 紧接着将上面的字符串运用md5加密一下
    // 其中 getKey() 就是咱们配置的 key("zhazha")
   String signatureValue = makeTokenSignature(expiryTime, username, password);
    // 紧接着将用户名, 过期时刻和上面核算的签名三个元素组成一个运用Base64加密的token
    // 创立一个 Cookie , 将值保存到cookie中, 并设置好过期时刻, 一般是 两周
   setCookie(new String[] { username, Long.toString(expiryTime), signatureValue }, tokenLifetime, request,
         response);
}

该类的注释都有这段阐明

image-20230108235804929

小白: “这儿是在认证时调用的remember-me功用, 怎样进行主动登录的呢? “

小黑: “在主动登录时, 通常会将在浏览器中的 cookie 中remember-me相关数据读取出来, 整个进程的开端, 就是下面这个办法”

// RememberMeAuthenticationFilter
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
      throws IOException, ServletException {
    // 假如现已登录, 那么就不需求remember-me功用了
   if (SecurityContextHolder.getContext().getAuthentication() != null) {
      chain.doFilter(request, response);
      return;
   }
    // 这儿触发了remember-me功用
   Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
   if (rememberMeAuth != null) {
      try {
          // 认证授权, 这儿显着需求
         rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
         // 将新的认证信息保存到SecurityContextHolder
         SecurityContext context = SecurityContextHolder.createEmptyContext();
         context.setAuthentication(rememberMeAuth);
         SecurityContextHolder.setContext(context);
          // 认证成功之后履行其他操作, 这个办法没有履行任何代码, 是给程序员自定义完成的
         onSuccessfulAuthentication(request, response, rememberMeAuth);
		  // 耐久化SecurityContext
         this.securityContextRepository.saveContext(context, request, response);
         if (this.eventPublisher != null) {
            this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
                  SecurityContextHolder.getContext().getAuthentication(), this.getClass()));
         }
         if (this.successHandler != null) {
            this.successHandler.onAuthenticationSuccess(request, response, rememberMeAuth);
            return;
         }
      }
      catch (AuthenticationException ex) {
         this.rememberMeServices.loginFail(request, response);
         onUnsuccessfulAuthentication(request, response, ex);
      }
   }
   chain.doFilter(request, response);
}

进入this.rememberMeServices.autoLogin(request, response)函数咱们会看到下面这段代码

// AbstractRememberMeServices
@Override
public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
    // 从 cookie 中拿到remember-me保存的数据
   String rememberMeCookie = extractRememberMeCookie(request);
   if (rememberMeCookie == null) {
      return null;
   }
    // 省掉了部分代码
   try {
       // 解分出cookie中原先的数据, 也就是上面那张图片
       // username + ":" + expiryTime + ":" + Md5Hex(username + ":" + expiryTime + ":" + password + ":" + key)
       // 而下面的代码跟 setCookie(new String[] { username, Long.toString(expiryTime), signatureValue }, tokenLifetime, request, response); 相对应 
       // 这儿有一个长度为 3 的数组, username, Long.toString(expiryTime), signatureValue
      String[] cookieTokens = decodeCookie(rememberMeCookie);
       // 所以你进入这个函数会发现cookieTokens进行了是否 length == 3 的判别
       // 这个函数内部还进行了主动登录处理
      UserDetails user = processAutoLoginCookie(cookieTokens, request, response);
       // 终究检测下, 是否lock, 是否过期和 账户是否 disable
      this.userDetailsChecker.check(user);
       // 认证成功的处理, 在这儿面创立了 RememberMeAuthenticationToken 目标, 并且填充了 Details 扩展
      return createSuccessfulAuthentication(request, user);
   }
   // 省掉了悉数 catch 代码
   // 删去掉浏览器中的关于remember-me的部分特点
   cancelCookie(request, response);
   return null;
}

接着进入processAutoLoginCookie(cookieTokens, request, response)类看看

// TokenBasedRememberMeServices
@Override
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request,
      HttpServletResponse response) {
    // 判别是否等于三个
    // 长度为 3 的数组, username, Long.toString(expiryTime), signatureValue
   if (cookieTokens.length != 3) {
      throw new InvalidCookieException(
            "Cookie token did not contain 3" + " tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
   }
    // 拿到时刻, 估计用于判别是否过期之类
   long tokenExpiryTime = getTokenExpiryTime(cookieTokens);
    // 判别是否过期
   if (isTokenExpired(tokenExpiryTime)) {
      throw new InvalidCookieException("Cookie token[1] has expired (expired on '" + new Date(tokenExpiryTime)
            + "'; current time is '" + new Date() + "')");
   }
   // 拿着 username 去数据库中读取, 可是这儿是内存中读取 username 对应的 User 目标
   UserDetails userDetails = getUserDetailsService().loadUserByUsername(cookieTokens[0]);
   // 通过现有的数据生成一个签名
   String expectedTokenSignature = makeTokenSignature(tokenExpiryTime, userDetails.getUsername(),
         userDetails.getPassword());
    // 然后拿着咱们生成签名和cookie中读取到的签名进行比较
   if (!equals(expectedTokenSignature, cookieTokens[2])) {
      throw new InvalidCookieException("Cookie token[2] contained signature '" + cookieTokens[2]
            + "' but expected '" + expectedTokenSignature + "'");
   }
    // 签名持平, 直接回来
   return userDetails;
}

小黑: “说白了, 这整个进程适当的简略, 从 cookie 读取的数据是一个数组, 数组中存放着 用户名, 过期时刻和运用过期时刻暗码和key等生成的签名”

小黑: “cookie中存放的 username 被用于查找数据库中匹配的用户, 终究生成签名预备和cookie中的签名进行匹配”

小黑: “cookie中的过期时刻, 首要用于判别remember-me的信息是否过期”

小黑: “cookie中的签名首要用于判别是否仍是那个”

小白: “哦哦, 看起来很简略啊, 首要是判别是否过期, 接着判别username拿到的user生成的签名和cookie中保存的签名是否相同, 完事”

小黑: “是的, 十分简略, 首要复杂的当地仍是加密和解密的进程, 和运用 ‘:’ 隔离的办法, 说白了, 整个进程适当的简略”

总结

小白: “进程虽然简略, 但我觉得你这也不安全吧? 不过是换了个名字么? 照样不安全吧?”

小黑: “spring security运用默许的remember-me肯定是不安全的, 你需求敞开耐久化功用, 也就是下图红框框的类”

image-20230109171227821

小白: “那你要怎样修改它内部运用哪个类的呢?”

小黑: “你有两种办法”

image-20230109173344864

image-20230109173410342

小黑: “也就是这两种办法”

image-20230109173627029

小黑: “从他们前面的源码能够看出, 这两种办法有优先级差异, 显着第一种优先级高于后面的tokenRepository

耐久化remember-me

耐久化令牌

@Bean
@Throws(Exception::class)
open fun securityFilterChain(httpSecurity: HttpSecurity): SecurityFilterChain {
    httpSecurity
        .authorizeRequests()
        .anyRequest().authenticated() // 任何账户暗码认证的用户都能够拜访
        httpSecurity.formLogin()
        .defaultSuccessUrl("/hello", true)
        val tokenRepository = JdbcTokenRepositoryImpl()
        tokenRepository.setCreateTableOnStartup(false)
        tokenRepository.dataSource = dataSource
        httpSecurity.rememberMe() // 发动rememberMe功用
        .tokenRepository(tokenRepository) // 只需运用了这个, 就会运用耐久化令牌办法
        httpSecurity.csrf().disable()
        // 不需求, 只需 userDetailsService 是一个 Spring Bean 就会被加载
        //    httpSecurity.userDetailsService(userDetailsService())
        return httpSecurity.build()
}

这儿我运用了kotlin, 意图有三

第一: 温习 kotlin 怎样运用, kotlin学了, 发现后端基本用不到, 怕忘记了

第二: kotlin代码也看得懂, 并且代码简略(我比较懒)

第三: 各位看官能够在熟悉kotlin的一起, 将代码修改为 java, 学习最忌讳眼高手低不是?

其实还有一点, 有人跟我赌, 说kotlin真的能够彻底替代java, 我不信!!!至少nacos不可, 会呈现问题, 我再试试spring security行不可, 不过看spring官方给出的案例代码都会有kotlin版本, 估计spring全家桶没问题

小黑: “只需添加上面的办法, 咱们就能够运用耐久化类计划PersistentTokenBasedRememberMeServices

小黑: “上面运用的是JdbcTokenRepositoryImpl办法, 将数据存储到数据库中”

image-20230110015918042

小黑: “这儿面会有表结构, 注意这儿的series是主键哦, 只需咱们这样”

image-20230110020143667

public void setCreateTableOnStartup(boolean createTableOnStartup) {
   this.createTableOnStartup = createTableOnStartup;
}

也就是这样

@Bean
@Throws(Exception::class)
open fun securityFilterChain(httpSecurity: HttpSecurity): SecurityFilterChain {
    httpSecurity.
        // 代码省掉
        val tokenRepository = JdbcTokenRepositoryImpl()
        // 要害仍是这行, 这样才会创立表结构
        tokenRepository.setCreateTableOnStartup(true)
        tokenRepository.dataSource = dataSource
        httpSecurity.rememberMe() // 发动rememberMe功用
        .tokenRepository(tokenRepository)
        return httpSecurity.build()
}

耐久化的运用其实没什么的, 十分简略, 中心仍是源码

耐久化remember-me源码剖析

关于remember-me耐久化办法的源码剖析我分为两个步骤

  1. 认证时, 发动remember-me
  2. 在浏览器或许服务器被封闭, 无法拿到用户信息时, 发动 remember-me 功用

认证时

发动的话仍是环绕这个办法

AbstractRememberMeServices#loginSuccess

@Override
public final void loginSuccess(HttpServletRequest request, HttpServletResponse response,
      Authentication successfulAuthentication) {
   if (!rememberMeRequested(request, this.parameter)) {
      this.logger.debug("Remember-me login not requested.");
      return;
   }
   onLoginSuccess(request, response, successfulAuthentication);
}

或许说环绕这这个办法onLoginSuccess

protected abstract void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication);

这是一个抽象办法, 实际的办法在这儿完成

PersistentTokenBasedRememberMeServices#onLoginSuccess

@Override
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
      Authentication successfulAuthentication) {
    // 拿到用户名
   String username = successfulAuthentication.getName();
    // 创立PersistentRememberMeToken
    // 运用 SecureRandom 生成 Series 和 Token, 这是一个随机字符串(细节就不追了)
   PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, generateSeriesData(),
         generateTokenData(), new Date());
   try {
       // 这儿将会调用咱们填写的 JdbcTokenRepositoryImpl 将生成的目标耐久化到数据库中
      this.tokenRepository.createNewToken(persistentToken);
       // 终究将PersistentRememberMeToken添加到 cookie中
       // setCookie(new String[] { token.getSeries(), token.getTokenValue() }, getTokenValiditySeconds(), request, response);
       // 第一个参数就是咱们生成的series和token
       // 第二个参数就是过期时刻, 默许是两周
       // 第三个参数和第四个参数就不必多说了
      addCookie(persistentToken, request, response);
   }
   catch (Exception ex) {
      this.logger.error("Failed to save persistent token ", ex);
   }
}

小白: “一头雾水, 为什么耐久化办法和内存办法不同? 耐久化办法怎样多了两个东西 seriestoken ?”

小黑: “现在跟你说, 你或许也不明白, 不过大体的意思是, series 只需客户运用用户名和暗码登录体系就会生成一个, 而 token 是每次发生一次会话就会生成一个新的”

小黑: “换句话说, 主动登录不会导致 series 改变”

小白: “这么做有什么好处么?”

小黑: “你不是说安全么? 每次都会生成一个新的, 能不安全么? 并且最要害的是 每一个 username 都会有多个 tokenseries, 那么不就完成了在多端登录时, 体系就能够判别咱们的账户是否被盗”

触发主动登录

主动登录咱们仍是得看这儿

AbstractRememberMeServices#autoLogin

@Override
public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
    // 从前端cookie的remember-me特点中拿到相对应的字符串
   String rememberMeCookie = extractRememberMeCookie(request);
   if (rememberMeCookie == null) {
      return null;
   }
   if (rememberMeCookie.length() == 0) {
      cancelCookie(request, response);
      return null;
   }
   try {
       // 解码出 series 和 token
      String[] cookieTokens = decodeCookie(rememberMeCookie);
       // 中心代码, 下面剖析
      UserDetails user = processAutoLoginCookie(cookieTokens, request, response);
       // 检测 User 是否 xx zz yy
      this.userDetailsChecker.check(user);
       // 创立RememberMeAuthenticationToken目标
      return createSuccessfulAuthentication(request, user);
   }
   cancelCookie(request, response);
   return null;
}

接下来是中心代码PersistentTokenBasedRememberMeServices

@Override
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request,
      HttpServletResponse response) {
    // 需求有 series 和 token 两个
   if (cookieTokens.length != 2) {
      throw new InvalidCookieException("Cookie token did not contain " + 2 + " tokens, but contained '"
            + Arrays.asList(cookieTokens) + "'");
   }
    // 拿到 series
   String presentedSeries = cookieTokens[0];
    // 拿到 token
   String presentedToken = cookieTokens[1];
    // 从数据库中依据 series 查询到对应的 token
   PersistentRememberMeToken token = this.tokenRepository.getTokenForSeries(presentedSeries);
    // 阐明数据库中没有相对应的 token
   if (token == null) {
      // 这样就不会走rememberme功用, 直接进入认证流程
      throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);
   }
   // token不匹配
   if (!presentedToken.equals(token.getTokenValue())) {
      // series 匹配, 可是 token不匹配, 则删去一切 username 的主动登录信息
      // 这时候阐明晰什么呢? 阐明主动登录的会话被刷新了, token值不对了
      // 那么就意味着该账号的信息被hacker完完整整的复制到其他电脑上拜访了, 这样hacker以为不运用用户暗码就能够凭借盗取的信息拜访, 但会导致原先的用户在拜访网站时, 呈现token不匹配问题, 就会删去该用户一切remember-me的功用
      // 这样即便黑客盗用了数据, 在正版用户拜访后, hacker也直接无法持续拜访了, 要运用用户名和暗码登录一次
      this.tokenRepository.removeUserTokens(token.getUsername());
   }
   // 判别当时时刻是否现已是超时时刻
   if (token.getDate().getTime() + getTokenValiditySeconds() * 1000L < System.currentTimeMillis()) {
      throw new RememberMeAuthenticationException("Remember-me login has expired");
   }
   // 阐明没超时, 验证也通过了
   // 那么创立一个新的 PersistentRememberMeToken 目标
   // 下面的办法创立了一个新的 token, 也就意味着每次触发主动登录, 都会创立一个新的 token
   PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(),
         generateTokenData(), new Date());
   try {
      // 更新到数据库
      this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
      // 添加到 cookie 中, 这样每次触发主动登录都会生成一个新的值存储到cookie中
      addCookie(newToken, request, response);
   }
   catch (Exception ex) {
      throw new RememberMeAuthenticationException("Autologin failed due to data access problem");
   }
   // 拿到用户目标
   return getUserDetailsService().loadUserByUsername(token.getUsername());
}

小白: “等下, 你回来的 User 目标终究拿来干嘛了?”

小黑: “拿出来当然是认证咯”

小白: “啊? 还要再认证一次? “

小黑: “是的, 需求认证一次, 不过这次走的不是DaoAuthenticationProvider认证器”

小白: “啊? 为什么啊? 不是需求从头认证么?”

小黑: “你忘记了? 认证器都会有一个support函数啊?”

小黑: “你还记得remember-me的进程了么? 其中它创立了一个RememberMeAuthenticationToken目标”

小黑: “这不就意味着走DaoAuthenticationProvider认证器就不支持了么? 所以肯定不会走这儿, 可是走RememberMeAuthenticationProvider认证器的话, 只会做 hashcode 匹配, 其实也没什么好讲的了”

小黑: “直接看源码吧”

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
   if (!supports(authentication.getClass())) {
      return null;
   }
   if (this.key.hashCode() != ((RememberMeAuthenticationToken) authentication).getKeyHash()) {
      throw new BadCredentialsException(this.messages.getMessage("RememberMeAuthenticationProvider.incorrectKey",
            "The presented RememberMeAuthenticationToken does not contain the expected key"));
   }
   return authentication;
}

小黑: “这儿的key是可配置的”

小黑: “回到RememberMeAuthenticationFilter的源码”

// RememberMeAuthenticationFilter
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
      throws IOException, ServletException {
   if (SecurityContextHolder.getContext().getAuthentication() != null) {
      this.logger.debug(LogMessage
            .of(() -> "SecurityContextHolder not populated with remember-me token, as it already contained: '"
                  + SecurityContextHolder.getContext().getAuthentication() + "'"));
      chain.doFilter(request, response);
      return;
   }
    // 咱们的主动登录在这儿现已完成了
   Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
   if (rememberMeAuth != null) {
      try {
         // 在这儿做了认证 RememberMeAuthenticationProvider 也就是做了 hashCode比较
         rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
         // 保存到SecurityContextHolder
         SecurityContext context = SecurityContextHolder.createEmptyContext();
         context.setAuthentication(rememberMeAuth);
         SecurityContextHolder.setContext(context);
         // 用户登录完成之后履行的代码, 这儿给程序员承继完成功用用的
         onSuccessfulAuthentication(request, response, rememberMeAuth);
         // 将 SecurityContext 保存到数据库中
         this.securityContextRepository.saveContext(context, request, response);
         if (this.eventPublisher != null) {
            this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
                  SecurityContextHolder.getContext().getAuthentication(), this.getClass()));
         }
         if (this.successHandler != null) {
            // 认证成功后履行的代码
            this.successHandler.onAuthenticationSuccess(request, response, rememberMeAuth);
            return;
         }
      }
      catch (AuthenticationException ex) {
         // 主动登录失利
         this.rememberMeServices.loginFail(request, response);
         onUnsuccessfulAuthentication(request, response, ex);
      }
   }
   chain.doFilter(request, response);
}

整体代码剖析下来仍是很简略的

二次校验功用的完成

是什么?

咱们经常发现, 许多网站在拜访灵敏操作时, 会让你从头输入暗码, 这种功用就是二次校验功用

所以咱们需求将资源分为一般资源和灵敏资源, 这样就能够保证在一般资源时运用主动登录边能够直接拜访, 可是主动登录却无法拜访灵敏操作, 需求输入用户名和暗码认证一次才干拜访灵敏资源

怎样做?

httpSecurity
   .authorizeRequests()
   .antMatchers("/authentication", "/auth").fullyAuthenticated()
   .anyRequest().authenticated()
@RestController
class IndexController {
   @GetMapping("remember")
   fun remember(): String {
      return "remember"
   }
   @GetMapping("auth")
   fun auth(): String {
      return "auth"
   }
   @GetMapping("both")
   fun both(): String {
      return "both"
   }
   @GetMapping("authentication")
   fun authentication(): String {
      return JSONUtil.toJsonStr(SecurityContextHolder.getContext().authentication)
   }
}

这么玩就行, 十分简略

在上面的 Controller中, 咱们只需保护 “/authentication” 和 “/auth” 就行了

remember-me前后端别离

思路

前后端别离的办法十分简略, 前面咱们现已说了这种思路

前后端别离的办法只需保证 requestresponse 的读写都是JSON格式就好, 所以咱们只需剖析remember-me的进程中, 是否直接从 requestresponse 中读取或许写入的状况, 遇到这种状况直接重写该办法便可

要剖析 remember-me 的源码直接看

public interface RememberMeServices {
   Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);
   void loginFail(HttpServletRequest request, HttpServletResponse response);
   void loginSuccess(HttpServletRequest request, HttpServletResponse response,
         Authentication successfulAuthentication);
}

剖析该办法, 终究发现只有 loginSuccess 存在直接从 request 中读取数据的状况

// AbstractRememberMeServices
@Override
public final void loginSuccess(HttpServletRequest request, HttpServletResponse response,
      Authentication successfulAuthentication) {
   if (!rememberMeRequested(request, this.parameter)) {
      return;
   }
   onLoginSuccess(request, response, successfulAuthentication);
}
// AbstractRememberMeServices
protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
   if (this.alwaysRemember) {
      return true;
   }
   // 重视这一行代码, 阐明咱们需求重写该办法了
   String paramValue = request.getParameter(parameter);
   if (paramValue != null) {
      if (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on")
            || paramValue.equalsIgnoreCase("yes") || paramValue.equals("1")) {
         return true;
      }
   }
   return false;
}

小白: “等等, 你没发现问题么?”

小黑: “什么问题?”

小白: “假如你需求重写上面那个办法, 那么你就必须从 request 中读取流”

小黑: “哦, 我知道你说啥了, 你是说前后端别离自身认证的当地就需求从 request 中读取前面的JSON恳求, 然后 remember-me 也需求从 request 中读取流, 这样, request 流被读取了两次”

小黑: “可是流的读取只能读取一次, 显着第二次读取request流会报错”

小白: “是的, 那你应该怎样解决呢?”

小黑: “思路也很简略, 认证和remember-me功用有先后顺序, 那么咱们读取request流肯定只能在认证时读取, 此刻咱们不只读取 usernamepassword 还得从request流中将rememeber-me读取读取出来, 接着要害步骤将读取出来的remember-me写入到requestattribute就行了”

在认证时

String memberMeValue = (String) map.get(myRememberMeService.getParameter());
request.setAttribute(myRememberMeService.getParameter(), memberMeValue);

rememberMeRequested读取时修改成下面这样

// 这儿在认证器的时候, 需求读取出来保存到 request 的 attribute 中
String paramValue = (String) request.getAttribute(parameter);

小黑: “思路完成, 仍是十分简略的, 不过完成的办法有许多, 我这只是其中一种算了”

办法

class LoginFilter(private val code: String) : UsernamePasswordAuthenticationFilter() {
	@Resource
	lateinit var objectMapper: ObjectMapper
	override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication {
		if (request.method != "POST") {
			throw AuthenticationServiceException("Authentication method not supported: " + request.method)
		}
		if (!LoginFilter::objectMapper.isLateinit) {
			objectMapper = ObjectMapper()
		}
		val contentType = request.contentType
		val session = request.session
		val verifyCode = session.getAttribute(VERIFY_CODE) as String
		if (StrUtil.equalsIgnoreCase(contentType, MediaType.APPLICATION_JSON_VALUE)
			|| StrUtil.equalsIgnoreCase(contentType, MediaType.APPLICATION_JSON_UTF8_VALUE)
		) {
			val map = objectMapper.readValue(request.inputStream, Map::class.java)
			// 图片验证等等
			val code = map[this.code] as String
			if (StrUtil.isBlank(verifyCode) || StrUtil.isBlank(code) || !StrUtil.equalsIgnoreCase(verifyCode, code)) {
				throw VerifyCodeException("图片验证失利")
			}
			// remember-me 功用
			val rememberMe = map[REMEMBER_ME] as? String
			request.setAttribute(REMEMBER_ME, rememberMe)
			// 账户验证
			var username = map[this.usernameParameter] as? String
			username = username?.trim() ?: ""
			var password = map[this.passwordParameter] as? String
			password = password?.trim() ?: ""
			val passwordAuthenticationToken = UsernamePasswordAuthenticationToken.unauthenticated(username, password)
			return this.authenticationManager.authenticate(passwordAuthenticationToken)
		}
		throw UsernameNotFoundException("恳求JSON格式不对")
	}
}
class RememberMeService(
	key: String,
	userDetailsService: UserDetailsService,
	tokenRepository: PersistentTokenRepository
) : PersistentTokenBasedRememberMeServices(key, userDetailsService, tokenRepository) {
	private val alwaysRememberF: Boolean
	init {
		val clazz = AbstractRememberMeServices::class.java
		val field = clazz.getDeclaredField("alwaysRemember")
		field.isAccessible = true
		alwaysRememberF = field.getBoolean(this)
	}
	override fun rememberMeRequested(request: HttpServletRequest, parameter: String): Boolean {
		if (alwaysRememberF) {
			return true
		}
		val paramValue = request.getAttribute(parameter) as? String
		if (paramValue != null) {
			if (paramValue.equals("true", ignoreCase = true) || paramValue.equals("on", ignoreCase = true)
				|| paramValue.equals("yes", ignoreCase = true) || paramValue == "1"
			) {
				return true
			}
		}
		logger.debug(
			LogMessage.format("Did not send remember-me cookie (principal did not set parameter '%s')", parameter)
		)
		return false
	}
}
@Configuration
open class SecurityConfig {
	@Resource
	lateinit var authenticationConfiguration: AuthenticationConfiguration
	@Bean
	open fun authenticationManager(): AuthenticationManager {
		return authenticationConfiguration.authenticationManager
	}
	@Bean
	open fun userDetailsService(): UserService {
		return UserService(authenticationManager())
	}
	@Bean
	open fun passwordEncoder(): PasswordEncoder {
		return PasswordEncoderFactories.createDelegatingPasswordEncoder()
	}
	@Bean
	open fun objectMapper(): ObjectMapper {
		return ObjectMapper()
	}
	@Bean
	open fun loginFilter(): LoginFilter {
		val loginFilter = LoginFilter("verify-code")
		loginFilter.setAuthenticationManager(authenticationManager())
		// 这儿需求给 认证器 履行 rememberMeService
		loginFilter.rememberMeServices = rememberMeService()
		loginFilter.setAuthenticationSuccessHandler { _, response, authentication ->
			val map = mutableMapOf<String, Any>()
			map["msg"] = "认证成功"
			map["status"] = HttpStatus.ACCEPTED.value()
			map["user"] = authentication
			response.contentType = MediaType.APPLICATION_JSON_UTF8_VALUE
			response.writer.println(JSONUtil.toJsonStr(map))
		}
		loginFilter.setAuthenticationFailureHandler { _, response, exception ->
			exception.printStackTrace()
			val map = mutableMapOf<String, Any>()
			map["msg"] = "认证失利"
			map["status"] = HttpStatus.FORBIDDEN.value()
			map["exception"] = exception.message!!
			response.contentType = MediaType.APPLICATION_JSON_UTF8_VALUE
			response.writer.println(JSONUtil.toJsonStr(map))
		}
		return loginFilter
	}
	@Bean
	open fun tokenRepository(): JdbcTokenRepositoryImpl {
		val tokenRepository = JdbcTokenRepositoryImpl()
		tokenRepository.setCreateTableOnStartup(false)
		tokenRepository.dataSource = dataSource
		return tokenRepository
	}
	@Bean
	open fun rememberMeService(): RememberMeService {
		return RememberMeService(UUID.fastUUID().toString(true), userDetailsService(), tokenRepository())
	}
	@Resource
	lateinit var dataSource: DataSource
	@Bean
	open fun securityFilterChain(httpSecurity: HttpSecurity): SecurityFilterChain {
		httpSecurity
			.authorizeRequests()
			.antMatchers("/verify-code").permitAll()
			.antMatchers("/authentication", "/auth").fullyAuthenticated()
			.anyRequest().authenticated()
		httpSecurity.formLogin()
			.permitAll()
		httpSecurity.rememberMe() // 发动rememberMe功用
			.tokenRepository(tokenRepository())
		httpSecurity.csrf().disable()
		// 不需求, 只需 userDetailsService 是一个 Spring Bean 就会被加载
//		httpSecurity.userDetailsService(userDetailsService())
		httpSecurity.addFilterBefore(loginFilter(), UsernamePasswordAuthenticationFilter::class.java)
		httpSecurity.exceptionHandling {
			it.authenticationEntryPoint { _, response, authException ->
				authException.printStackTrace()
				val map = mutableMapOf<String, Any>()
				map["msg"] = "认证失利, 请从头认证"
				map["status"] = HttpStatus.UNAUTHORIZED.value()
				map["exception"] = authException.message!!
				response.contentType = MediaType.APPLICATION_JSON_UTF8_VALUE
				response.writer.println(JSONUtil.toJsonStr(map))
			}
			it.accessDeniedHandler { _, response, accessDeniedException ->
				accessDeniedException.printStackTrace()
				val map = mutableMapOf<String, Any>()
				map["msg"] = "您没有权限拜访"
				map["status"] = HttpStatus.FORBIDDEN.value()
				map["exception"] = accessDeniedException.message!!
				response.contentType = MediaType.APPLICATION_JSON_UTF8_VALUE
				response.writer.println(JSONUtil.toJsonStr(map))
			}
		}
		httpSecurity.logout {
			it.logoutSuccessHandler { _, response, authentication ->
				val map = mutableMapOf<String, Any>()
				map["msg"] = "刊出成功"
				map["status"] = HttpStatus.ACCEPTED.value()
				map["user"] = authentication
				response.contentType = MediaType.APPLICATION_JSON_UTF8_VALUE
				response.writer.println(JSONUtil.toJsonStr(map))
			}
			it.permitAll()
		}
		return httpSecurity.build()
	}
}
@RestController
class IndexController {
	@GetMapping("auth")
	fun auth(): String {
		return "auth"
	}
	@GetMapping("both")
	fun both(): String {
		return "both"
	}
	@GetMapping("authentication")
	fun authentication(): String {
		return JSONUtil.toJsonStr(SecurityContextHolder.getContext().authentication)
	}
	@GetMapping("/verify-code")
	fun verifyCodeImage(session: HttpSession, response: HttpServletResponse): Unit {
		val captcha = CaptchaUtil.createGifCaptcha(80, 50, 4)
		session.setAttribute(VERIFY_CODE, captcha.code)
		response.contentType = MediaType.IMAGE_GIF_VALUE
		response.outputStream.use {
			captcha.write(it)
		}
	}
}

代码看起来就十分简略了

完整代码能够看这儿: remember_me2-spring-security