Spring Security 自定义记住我功能!

目录

一、介绍

二、源码分析           

          ​编辑

三、传统web应用

四、前后端分离应用

4.1 自定义认证类 LoginFilter

4.2 ⾃定义 RememberMeService

4.3 配置应用


一、介绍

 本篇文章从方面进行讲解 :

  •   传统web 应用场景
  •   前后端分离应用场景       

二、源码分析           

        AbstractUserDetailsAuthenticationProvider类中authenticate⽅法在最后认证成功之后实现了记住我功能,但是查看源码得知如果开启记住我,必须进⾏相关的设置。

          6d3e8bc5e2f33dde0e73c230b0b92bbd.png

通过对源码的分析得出自定义的实现方式:

  1. 传统web方式
    1. 需要在所添加页面内添加一个 remember-me 的参数 值 为 true on yes 1
    2. 在配置类开启rememberMe 功能
  2. 前后端分离方式
    1. 自定义LoginFilter 继承 UsernamePasswordAuthenticationFilter 重写 attemptAuthentication 修改通 JSON 的方式获取值
    2. 自定义 RememberMeServices 的实现类 ,去重写rememberMeRequestd 方法,来修改成通过其他方式获取remember-me的值。
    3. 将重写后的类配置到 WebSecurityConfigurerAdapter 的继承类中
    4. 注意 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;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值