spring-security使用-登录(一) Form表单登录 ajax登录+验证码登录 自动登录  持久化令牌

默认登录

1.pom配置

  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

2.java类

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

3.启动项目

可以发现日志打印了了密码默认是user用户

spring-security使用-登录(一)
Form表单登录
ajax登录+验证码登录
自动登录 
持久化令牌

3.访问

localhost:8080/hello 重定向到了http://localhost:8080/login

spring-security使用-登录(一)
Form表单登录
ajax登录+验证码登录
自动登录 
持久化令牌

3.输入用户名密码再访问

用户名:user 密码:7e6a1360-1115-4eb6-8e17-5308164e5b26

spring-security使用-登录(一)
Form表单登录
ajax登录+验证码登录
自动登录 
持久化令牌

4.为什么是给user生成的密码如何生成

查看UserDetailsServiceAutoConfiguration此类

org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration#getOrDeducePassword

 private String getOrDeducePassword(User user, PasswordEncoder encoder) {
        //user获得密码
String password
= user.getPassword(); if (user.isPasswordGenerated()) {
//这里就是控制台打印的日志 logger.info(String.format(
"%n%nUsing generated security password: %s%n", user.getPassword())); } return encoder == null && !PASSWORD_ALGORITHM_PATTERN.matcher(password).matches() ? "{noop}" + password : password; }

5.我们再看User

这里一目了然 默认是user和uuid生成用户名和密码,我们可以通过在配置文件配置修改

spring.security.user.name=liqiang
spring.security.user.password=liqiang
@ConfigurationProperties(
        prefix = "spring.security"//说明我们可以通过配置文件配置密码和user
)
public class SecurityProperties {
    private SecurityProperties.User user = new SecurityProperties.User();

    public static class User {
        //用户 默认user
        private String name = "user";
        //使用uuid生成密码
        private String password = UUID.randomUUID().toString();
        private List<String> roles = new ArrayList();
        private boolean passwordGenerated = true;

        public void setPassword(String password) {
            //密码是否不为空
            if (StringUtils.hasLength(password)) {
                //标签打为false  后面if (user.isPasswordGenerated()) 打印日志
                this.passwordGenerated = false;
                this.password = password;
            }
        }
    }
}

内存中多用户配置

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 对密码进行加密的实例
     * @return
     */
    @Bean
    PasswordEncoder passwordEncoder() {
        /**
         * 不加密所以使用NoOpPasswordEncoder
         * 更多可以参考PasswordEncoder 的默认实现官方推荐使用: BCryptPasswordEncoder,BCryptPasswordEncoder
         */
        return NoOpPasswordEncoder.getInstance();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        /**
         *  inMemoryAuthentication 开启在内存中定义用户
         *  多个用户通过and隔开
         */
        auth.inMemoryAuthentication()
                .withUser("liqiang").password("liqiang").roles("admin")
                 .and()
                .withUser("admin").password("admin").roles("admin");
    }
}
PasswordEncoder为防止密码泄露,数据库保存的是加密的密码,然后前端登录传过来加密后根据数据库的进行匹配 我们可以自己实现和用默认的

自定义登录页面

security的默认登录页面不能满足我们需求,我们大多数场景都需要自定义登录页面

1.增加自定义登录页面html

spring-security使用-登录(一)
Form表单登录
ajax登录+验证码登录
自动登录 
持久化令牌

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!--会get请求login.html 提交则是post login--> <form action="/login.html" method="post"> <div class="input"> <label for="name">用户名</label> <input type="text" name="username" > <span class="spin"></span> </div> <div class="input"> <label for="pass">密码</label> <input type="password" name="password" > <span class="spin"></span> </div> <div class="button login"> <button type="submit"> <span>登录</span> <i class="fa fa-check"></i> </button> </div> </form> </body> </html>

2.配置自定义html页面路径

 /**
     * 对于不需要授权的静态文件放行
     *
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/js/**", "/css/**", "/images/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()//form表单的方式
                .loginPage("/login.html")//登录页面路径
                .permitAll()//不拦截
                .and()
                .csrf()//记得关闭
                .disable();
    }

3.再次访问就是显示自定义的登录页面

spring-security使用-登录(一)
Form表单登录
ajax登录+验证码登录
自动登录 
持久化令牌

自定义登录请求地址

1.如果我们引入security 什么都不做那么

get http://localhost:8080/login  登录页面

post  http://localhost:8080/login 登录请求

我们前面自定义了登录页面请求但是请求地址action配置的是否可以自定义为其他

<form action="/login.html" method="post">

2.我们可以通过后台配置

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login.html")//登录页面路径
                .loginProcessingUrl("/doLogin")
                .permitAll()//不拦截
                .and()
                .csrf()//记得关闭
                .disable();
    }

我们的form表单action配置就可以改为

<form action="/doLogin" method="post">

登录url自定义和处理登录的源码处

默认配置是在哪里配置的呢可以看org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer

    public FormLoginConfigurer() {
//<1>调用父类的 详情看里面
super(new UsernamePasswordAuthenticationFilter(), (String)null);
//用户名密码默认参数名
this.usernameParameter("username"); this.passwordParameter("password"); }

<1>org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer#AbstractAuthenticationFilterConfigurer()

protected AbstractAuthenticationFilterConfigurer(F authenticationFilter, String defaultLoginProcessingUrl) {
        
<2>
this(); this.authFilter = authenticationFilter; if (defaultLoginProcessingUrl != null) { this.loginProcessingUrl(defaultLoginProcessingUrl); } }

<2>org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer.AbstractAuthenticationFilterConfigurer

  protected AbstractAuthenticationFilterConfigurer() {
        this.defaultSuccessHandler = new SavedRequestAwareAuthenticationSuccessHandler();
        this.successHandler = this.defaultSuccessHandler;
//可以看到默认是login
this.setLoginPage("/login"); }

4.默认登录请求路径的源码处

org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer#init

 public void init(H http) throws Exception {
//<1>调用了父类的init
super.init(http); this.initDefaultLoginFilter(http); }

<1>org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer#init

    public void init(B http) throws Exception {
<2>
this.updateAuthenticationDefaults(); this.updateAccessDefaults(http); this.registerDefaultAuthenticationEntryPoint(http); }

<2>

org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer#updateAuthenticationDefaults

protected final void updateAuthenticationDefaults() {
       //没有手动设置 则默认用loginPage
        if (this.loginProcessingUrl == null) {
            this.loginProcessingUrl(this.loginPage);
        }

        if (this.failureHandler == null) {
            this.failureUrl(this.loginPage + "?error");
        }

        LogoutConfigurer<B> logoutConfigurer = (LogoutConfigurer)((HttpSecurityBuilder)this.getBuilder()).getConfigurer(LogoutConfigurer.class);
        if (logoutConfigurer != null && !logoutConfigurer.isCustomLogoutSuccess()) {
            logoutConfigurer.logoutSuccessUrl(this.loginPage + "?logout");
        }

    }

自定义登录用户名密码参数

1.默认情况下 用户名为username 密码为password为必须

  <div class="input">
        <label for="name">用户名</label>
        <input type="text" name="username" >
        <span class="spin"></span>
    </div>
    <div class="input">
        <label for="pass">密码</label>
        <input type="password" name="password" >
        <span class="spin"></span>
    </div>

2.我们可以通过配置修改

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login.html")//登录页面路径
                .loginProcessingUrl("/doLogin")
                .usernameParameter("loginName")
                .passwordParameter("loginPassword")
                .permitAll()//不拦截
                .and()
                .csrf()//记得关闭
                .disable();
    }
}

自定义登录用户名密码参数源码处

org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer

public FormLoginConfigurer() {
        super(new UsernamePasswordAuthenticationFilter(), (String)null);
//设置参数名字
this.usernameParameter("username"); this.passwordParameter("password"); }

2.可以发现最终是调用UsernamePasswordAuthenticationFilter对象的set方法写入

org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer#usernameParameter

org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer#passwordParameter

 public FormLoginConfigurer<H> usernameParameter(String usernameParameter) {
((UsernamePasswordAuthenticationFilter)
this.getAuthenticationFilter()).setUsernameParameter(usernameParameter); return this; } public FormLoginConfigurer<H> passwordParameter(String passwordParameter) { ((UsernamePasswordAuthenticationFilter)this.getAuthenticationFilter()).setPasswordParameter(passwordParameter); return this; }

3.fitler内部根据此配置去获取用户名密码

org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#attemptAuthentication

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
//<1> String username
= this.obtainUsername(request); String password = this.obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); this.setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } }

<1>

 @Nullable
    protected String obtainPassword(HttpServletRequest request) {
//通过我们配置的名字获取
return request.getParameter(this.passwordParameter); } @Nullable protected String obtainUsername(HttpServletRequest request) {
//通过我们配置的名字获取
return request.getParameter(this.usernameParameter); }

自定义登录跳转

1.security可通过defaultSuccessUrl、successForwardUrl指定登录成功跳转页面

defaultSuccessUrl 如果是通过访问其他页面无权访问重定向到登录页面登录,登录成功则跳转到来源页面,如果是直接访问登录页面则直接跳转到指定url

successForwardUrl 登录成功不管来源直接跳转到指定页面 

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login.html")//登录页面路径
                .loginProcessingUrl("/doLogin")
                .usernameParameter("loginName")
                .passwordParameter("loginPassword")
                .defaultSuccessUrl("/index")
                .successForwardUrl("/index")
                .permitAll()//不拦截
                .and()
                .csrf()//记得关闭
                .disable();
    }

自定义登录失败跳转

failureForwardUrl 登录失败服务器重定向

failureUrl 登录失败前端302重定向地址

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login.html")//登录页面路径
                .loginProcessingUrl("/doLogin")
                .usernameParameter("loginName")
                .passwordParameter("loginPassword")
                .defaultSuccessUrl("/index")
                .successForwardUrl("index")
                .failureForwardUrl("/loginFail")
                .failureUrl("/login.html")
                .permitAll()//不拦截
                .and()
                .csrf()//记得关闭
                .disable();
    }
}

注销登录

.and()
.logout()
.logoutUrl("/logout")//自定义注销地址
.logoutRequestMatcher(new AntPathRequestMatcher("/logout","POST"))  //自定义请求方式
.logoutSuccessUrl("/login.html") //注销后跳转页面
.deleteCookies()//清除cookie
.clearAuthentication(true)//清除权限相关
.invalidateHttpSession(true)//清除session
.permitAll()
.and()

ajax登录+验证码登录

1.自定义登录处理器

public class CustomizeUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
        //从session获得验证码
        String verify_code = (String) request.getSession().getAttribute("verify_code");
        //判断是否是json post请求 如果不是则走父类的form登录
        if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
            Map<String, String> loginData = new HashMap<>();
            try {
                //解析bodyjson数据转为Map
                loginData = new ObjectMapper().readValue(request.getInputStream(), Map.class);
            } catch (IOException e) {
                throw new AuthenticationServiceException("系统异常");
            }
            String code = loginData.get("code");
            //检查验证码
            checkCode(response, code, verify_code);
            //获得用户输入的用户名和密码 如果是form登录的话上面配置的用户名key和密码key就不会起效 用默认的
            String username = loginData.get(getUsernameParameter());
            String password = loginData.get(getPasswordParameter());
            if (username == null) {
                username = "";
            }
            if (password == null) {
                password = "";
            }
            username = username.trim();
            //模拟父类的实现
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                    username, password);
            setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        } else {
            checkCode(response, request.getParameter("code"), verify_code);
            //如果是form登录直接使用父类的
            return super.attemptAuthentication(request, response);
        }
    }
    public void checkCode(HttpServletResponse resp, String code, String verify_code) {
        if (code == null || verify_code == null || "".equals(code) || !verify_code.toLowerCase().equals(code.toLowerCase())) {
            //验证码不正确
            throw new AuthenticationServiceException("验证码不正确");
        }
    }
}

2.初始化登录处理器并重写登录成功和登录失败的逻辑

com.liqiang.demo.configs.SecurityConfig#initLoginFilter

 public Filter initLoginFilter() throws Exception {
        CustomizeUsernamePasswordAuthenticationFilter customizeUsernamePasswordAuthenticationFilter= new CustomizeUsernamePasswordAuthenticationFilter();
        /**
         * 授权成功处理器 可以看父类里面默认就是这2个
         */
        SavedRequestAwareAuthenticationSuccessHandler handler=new SavedRequestAwareAuthenticationSuccessHandler();
        AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
        customizeUsernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                //此处我们做了兼容 如果是json 则响应json 否则走form默认的逻辑
                if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
                    response.setContentType("application/json;charset=utf-8");
                    PrintWriter out = response.getWriter();
                    Map<String,Object> result=new HashMap<>();
                    result.put("code",200);
                    result.put("message","登录成功");
                    String s = new ObjectMapper().writeValueAsString(result);
                    out.write(s);
                    out.flush();
                    out.close();
                }else{
                    //表单登录委托给默认的处理器
                    handler.onAuthenticationSuccess(request,response,authentication);
                }
            }
        });
        //授权失败处理器
        customizeUsernamePasswordAuthenticationFilter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
            @Override
            public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
                //此处我们做了兼容 如果是json 则响应json 否则走form默认的逻辑
                if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
                    response.setContentType("application/json;charset=utf-8");
                    PrintWriter out = response.getWriter();
                    Map<String,Object> result=new HashMap<>();
                    result.put("code",200);
                    result.put("message","登录失败");
                    if (exception instanceof LockedException) {
                        result.put("code",901);
                        result.put("message","账户被锁定,请联系管理员!");
                    } else if (exception instanceof CredentialsExpiredException) {
                        result.put("code",902);
                        result.put("message","密码过期,请联系管理员!");
                    } else if (exception instanceof AccountExpiredException) {
                        result.put("code",903);
                        result.put("message","账户过期,请联系管理员!");
                    } else if (exception instanceof DisabledException) {

                        result.put("code",904);
                        result.put("message","账户被禁用,请联系管理员!");
                    } else if (exception instanceof BadCredentialsException) {
                        result.put("code",905);
                        result.put("message","用户名或者密码输入错误,请重新输入!");
                    }
                    out.write(new ObjectMapper().writeValueAsString(result));
                    out.flush();
                    out.close();
                }else{
                    //表单登录委托给默认的处理器
                    failureHandler.onAuthenticationFailure(request,response,exception);
                }
            }
        });
        customizeUsernamePasswordAuthenticationFilter.setAuthenticationManager(super.authenticationManagerBean());
        //拦截处理的url
        customizeUsernamePasswordAuthenticationFilter.setFilterProcessesUrl("/doLogin");
        return customizeUsernamePasswordAuthenticationFilter;
    }

3.替换默认的登录处理器

   @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login.html")//登录页面路径
                .loginProcessingUrl("/doLogin")
                .usernameParameter("loginName")
                .passwordParameter("loginPassword")
                .defaultSuccessUrl("/index")
                .successForwardUrl("/index")
                .failureForwardUrl("/loginFail")
                .failureUrl("/login.html")
                .permitAll()//不拦截
                .and()
                //替换默认的登录处理器 注意form的配置将不起效果 可看formLogin源码 并没有替换 比如 usernameParameter passwordParameter
                .addFilterAt(initLoginFilter(), UsernamePasswordAuthenticationFilter.class)
                .csrf()//记得关闭
                .disable();
    }

4.测试

spring-security使用-登录(一)
Form表单登录
ajax登录+验证码登录
自动登录 
持久化令牌

自动登录 

简单实用

1.在原来的基础上增加记住密码配置

   @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and().rememberMe()
                .and()
                .formLogin()
                .usernameParameter("loginName")
                .passwordParameter("loginPassword")
                .defaultSuccessUrl("/hello")
                .failureForwardUrl("/loginFail")
                .failureUrl("/login.html")
                .permitAll()//不拦截
                .and()
                //替换默认的登录处理器 注意顺序 因为下面有给拦截器的属性赋值 比如 usernameParameter passwordParameter
                .addFilterAt(initLoginFilter(), UsernamePasswordAuthenticationFilter.class)
                .csrf()//记得关闭
                .disable();
    }

spring-security使用-登录(一)
Form表单登录
ajax登录+验证码登录
自动登录 
持久化令牌

原理

1.在登录时发现提交参数增加了一个remember-me: on

spring-security使用-登录(一)
Form表单登录
ajax登录+验证码登录
自动登录 
持久化令牌

2.退出浏览器都会默认带上cookie

Cookie:
JSESSIONID=5401CED03129ED6CF941CF9812170EAE; remember-me=bGlxaWFuZzoxNjEwMDk0MzgyMDQ2OjVlODZkOTc2OTY0M2UzMmZlZjUyNTgzYWU0NjhhNTJh

3.将remember-me的值通过base 64解密发现

值为:  liqiang:1610094382046:5e86d9769643e32fef52583ae468a52a

第一个为用户名 第二个为过期时间 第三个为加密key 

4.remomber-me生成源码

org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices

 public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
        //从登录Authentication 获取用户名和密码
        String username = this.retrieveUserName(successfulAuthentication);
        String password = this.retrievePassword(successfulAuthentication);
        if (!StringUtils.hasLength(username)) {
            this.logger.debug("Unable to retrieve username");
        } else {
            //因为登录成功密码有可能被其他filter擦除 这里如果没有再查一次
            if (!StringUtils.hasLength(password)) {
                UserDetails user = this.getUserDetailsService().loadUserByUsername(username);
                password = user.getPassword();
                if (!StringUtils.hasLength(password)) {
                    this.logger.debug("Unable to obtain password for user: " + username);
                    return;
                }
            }
            //获取过期时间秒默认是 应该是可配的  默认是1209600
            int tokenLifetime = this.calculateLoginLifetime(request, successfulAuthentication);
            long expiryTime = System.currentTimeMillis();
            //当前时间加上过期时间毫秒
            expiryTime += 1000L * (long)(tokenLifetime < 0 ? 1209600 : tokenLifetime);
            //算出加密值
            String signatureValue = this.makeTokenSignature(expiryTime, username, password);
            // 用户名+:+过期时间+:密码+:+加密值 写入cookie 并设置过期时间
            this.setCookie(new String[]{username, Long.toString(expiryTime), signatureValue}, tokenLifetime, request, response);
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Added remember-me cookie for user '" + username + "', expiry: '" + new Date(expiryTime) + "'");
            }

        }
    }
    protected String makeTokenSignature(long tokenExpiryTime, String username, String password) {
        //用户名+:+过期时间+:密码+:秘钥 这个getKey是uuid  每次重启 都需要重新登录 我们可以配置写死
String data = username + ":" + tokenExpiryTime + ":" + password + ":" + this.getKey(); MessageDigest digest; try { //进行md5加密 digest = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException var8) { throw new IllegalStateException("No MD5 algorithm available!"); } return new String(Hex.encode(digest.digest(data.getBytes()))); }

key配置

 protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and().rememberMe()
                .key("system")
                .and()
                .formLogin()
//                .loginPage("/login.html")//登录页面路径
//                .loginProcessingUrl("/doLogin")
                .usernameParameter("loginName")
                .passwordParameter("loginPassword")
                .defaultSuccessUrl("/hello")
                .failureForwardUrl("/loginFail")
                .failureUrl("/login.html")
                .permitAll()//不拦截
                .and()
                //替换默认的登录处理器 注意顺序 因为下面有给拦截器的属性赋值 比如 usernameParameter passwordParameter
                .addFilterAt(initLoginFilter(), UsernamePasswordAuthenticationFilter.class)
                .csrf()//记得关闭
                .disable();
    }

5.校验

org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter

  public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;
        if (SecurityContextHolder.getContext().getAuthentication() == null) {
            //先调用rememberMeServices 的autoLogin 取出cookie 进行解析校验有消息
            Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
            if (rememberMeAuth != null) {
                try {
                    rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
                    SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
                    this.onSuccessfulAuthentication(request, response, rememberMeAuth);
                    if (this.logger.isDebugEnabled()) {
                        this.logger.debug("SecurityContextHolder populated with remember-me token: '" + SecurityContextHolder.getContext().getAuthentication() + "'");
                    }

                    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 var8) {
                    if (this.logger.isDebugEnabled()) {
                        this.logger.debug("SecurityContextHolder not populated with remember-me token, as AuthenticationManager rejected Authentication returned by RememberMeServices: '" + rememberMeAuth + "'; invalidating remember-me token", var8);
                    }

                    this.rememberMeServices.loginFail(request, response);
                    this.onUnsuccessfulAuthentication(request, response, var8);
                }
            }

            chain.doFilter(request, response);
        } else {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("SecurityContextHolder not populated with remember-me token, as it already contained: '" + SecurityContextHolder.getContext().getAuthentication() + "'");
            }

            chain.doFilter(request, response);
        }

    }

持久化令牌

简单使用

好处是,每次获取令牌都是从数据库拿,如果有别的地方登陆 token就会改变,原有用户的token就校验不过。让用户感知账号被别的地方登陆

参考的org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl 实现 由查询数据库改为查redis

@Component
public class RedisPersistentTokenRepository implements PersistentTokenRepository {
    TokenRedisRepository tokenRedisRepository;
    PersistentRememberMeTokenConvert persistentRememberMeTokenConvert;
    @Override
    public void createNewToken(PersistentRememberMeToken persistentRememberMeToken) {
        PersistentRememberMeTokenRo persistentRememberMeTokenRo=persistentRememberMeTokenConvert.toRo(persistentRememberMeToken);
        //保存到redis hash方式 key为token:{series}
        tokenRedisRepository.save(persistentRememberMeToken);
    }

    @Override
    public void updateToken(String series, String tokenValue, Date lastUsed) {
        PersistentRememberMeTokenRo persistentRememberMeTokenRo=tokenRedisRepository.findOne(series);
        persistentRememberMeTokenRo.setDate(lastUsed);
        persistentRememberMeTokenRo.setTokenValue(tokenValue);
        //保存到redis hash方式 key为token:{userName}
        tokenRedisRepository.save(persistentRememberMeToken);
    }

    @Override
    public PersistentRememberMeToken getTokenForSeries(String series) {
        PersistentRememberMeTokenRo persistentRememberMeTokenRo=tokenRedisRepository.findOne(series);
        return persistentRememberMeTokenConvert.toPersistentRememberMeToken(persistentRememberMeTokenRo);;
    }

    @Override
    public void removeUserTokens(String series) {
        tokenRedisRepository.del(s);
    }
}

配置

 http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .rememberMe()
                .tokenRepository(getApplicationContext().getBean(RedisPersistentTokenRepository.class))
                .key("system")
                .and()
                .formLogin()
//                .loginPage("/login.html")//登录页面路径
//                .loginProcessingUrl("/doLogin")
                .usernameParameter("loginName")
                .passwordParameter("loginPassword")
                .defaultSuccessUrl("/hello")
                .failureForwardUrl("/loginFail")
                .failureUrl("/login.html")
                .permitAll()//不拦截
                .and()
                //替换默认的登录处理器 注意顺序 因为下面有给拦截器的属性赋值 比如 usernameParameter passwordParameter
                .addFilterAt(initLoginFilter(), UsernamePasswordAuthenticationFilter.class)
                .csrf()//记得关闭
                .disable();

源码

1.写入

org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices

  protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
        String username = successfulAuthentication.getName();
        this.logger.debug("Creating new persistent login for user " + username);
        //创建persistentTokend对象
        PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, this.generateSeriesData(), this.generateTokenData(), new Date());
        try {
            //持久化
            this.tokenRepository.createNewToken(persistentToken);
            //写入cookie
            this.addCookie(persistentToken, request, response);
        } catch (Exception var7) {
            this.logger.error("Failed to save persistent token ", var7);
        }

    }

2.校验

重写了父类的此方法

org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices#processAutoLoginCookie