使用SpringSecurity开发基于表单的认证

SpringSecurity核心功能

  • 认证

识别当前用户是否合法

即:你是谁

  • 授权

当前用户能访问的数据、页面权限

即:你能干什么

  • 攻击防护

防止伪造身份

SpringSecurity基本原理

案例演示

默认HttpBasic验证行为

  • 开启SpringSecurity配置

imooc-security-demo项目中,配置

使用SpringSecurity开发基于表单的认证

默认其实就是true

  • 访问接口

我们在浏览器中输入 http://localhost:8080/hello

发现并没有访问到接口,而是弹出一个认证窗口

使用SpringSecurity开发基于表单的认证

这个其实就是SpringSecurity默认行为搞的鬼

默认的用户名:user,密码在启动日志中能看到

使用SpringSecurity开发基于表单的认证

我们在认证窗口中填写好认证信息,就访问到了接口

使用SpringSecurity开发基于表单的认证

在引入了基于SpringBoot的SpringSecurity后,其默认行为会拦截保护所有对服务器的访问。
每个请求都需要用户名/密码认证
这种由SpringSecurity提供的默认校验方式叫做httpBasic
实际环境中基本没用,所以我们往往需要自定义

覆盖掉HttpBasic行为:跳转表单认证

通过继承WebSecurityConfigurerAdapter适配器类,实现我们自定义的SpringSecurity配置逻辑

  • 需求

HttpBasic的弹窗认证,改为表单认证

我们进行自定义配置

package com.imooc.security.browser;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;


@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {

//        设置认证方式
        http.formLogin()
                .and()
//                对请求进行授权
                .authorizeRequests()
//                认证请求
                .anyRequest()
//                都需要身份认证
                .authenticated();

    }
}

此时我们再次启动demo项目并进行访问,发现还是弹窗认证

原因是因为demo项目默认扫描的包并不包含BrowserSecurityConfig

我们对demo项目的启动类进行些许改造

package com.imooc.security.demo;

import com.imooc.security.browser.BrowserSecurityConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author sherry
 * @Description
 * @Date Create in 2019-03-13
 * @Modified By:
 */
@SpringBootApplication(scanBasePackageClasses = {DemoApplication.class, BrowserSecurityConfig.class})
@RestController
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class);
    }

    @GetMapping("/hello")
    public String hello() {
        return "Hello Spring Security";
    }
}

此时,我们访问/hello,认证界面为:

使用SpringSecurity开发基于表单的认证

我们访问/helloSpringSecurity发现这个接口需要认证,然后就自动跳转到了/login,然后就跳转到了默认的表单登陆页面

我们输入默认的user用户名和密码,就能够访问到/hello接口了

  • SpringSecurity默认行为配置

默认对所有接口进行弹窗认证,其配置如下

@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {

//        设置认证方式
        http.httpBasic()
                .and()
//                对请求进行授权
                .authorizeRequests()
//                认证请求
                .anyRequest()
//                都需要身份认证
                .authenticated();

    }
}

原理说明

  • 过滤器链机制

我们使用颜色对过滤器进行分类

使用SpringSecurity开发基于表单的认证

绿色

绿色的过滤器,代表一种认证方式,如UsernamePasswordAuthenticationFilter,处理表单登陆

BasicAuthenticationFilter,处理httpBasic

绿色的过滤器,检查当前的请求中,是否有当前过滤器所需的信息

UsernamePasswordAuthenticationFilter:首先判断当前是不是登陆请求,然后判断这个请求中有没有带用户名/密码,如果带了,则进行校验,如果没带,则放行,走下一个过滤器

BasicAuthenticationFilter:判断请求头中有没有Basic开头的认证信息,有则认证,没有,则放行,走下一个过滤器

任何一个绿色的过滤器,认证成功后,会对当前请求做一个标记,表示认证成功了

注意,所有的绿色过滤器都会生效。然后根据我们的请求参数符合哪个过滤器,哪个过滤器就会处理本次认证

蓝色

蓝色用于捕获橙色抛出来的异常,当认证不通过的时候,橙色是会抛出异常的

蓝色过滤器捕获到橙色过滤器的异常后,将异常传递给对应的绿色过滤器,进行认证不通过时的处理

如对于表单认证,就会跳转到表单登陆页面

橙色

橙色的过滤器,过滤器链的最后一层,由其决定是否对当前请求放行。一旦放行,就能够访问到REST API

判断依据为代码中的配置,如:

使用SpringSecurity开发基于表单的认证

就会要求所有的接口,必须被认证后才能访问

这个配置逻辑现在是比较简单的,事实上可以配置得非常复杂,如放行某些接口,某些接口只有某些客户才能访问等等

  • 过滤器类型
    只有图中绿色的过滤器是可以由我们控制是否生效,并且支持自定义的。另外两种类型的过滤器不是由我们控制的

源代码导读

我们阅读下面几个类

  • UsernamePasswordAuthenticationFilter
    负责表单登陆的过滤器

  • ExceptionTranslationFilter
    处理认证过程中的异常

  • FilterSecurityInterceptor
    判断认证是否通过

当我们直接访问API的时候,FilterSecurityInterceptor检测到认证未通过,异常被ExceptionTranslationFilter`捕获到,根据我们的配置,跳转到默认的登陆页面上。

在登陆页面中输入了正确的用户名密码,被UsernamePasswordAuthenticationFilter认证通过,最终FilterSecurityInterceptor确认可以访问,遂成功访问到API

用户名+密码认证

自定义用户认证逻辑

  • 怎么获取用户认证的信息?
  • 怎么校验用户认证信息?
  • 加密解密

获取用户信息:UserDetailsService

如何获取用户信息,在SpringSecurity中,是被封装在UserDetailsService接口中的

我们实现这个接口,将查询到的用户信息保存到UserDetails对象中

SpringSecurity会根据UserDetails中的密码,与用户请求中填写的密码进行比较

  • 小结

UserDetailsService:接口用于获取用户身份信息
UserDetails:接口用于保存获取到的用户信息
org.springframework.security.core.userdetails.User:是Spring Security为我们提供的对UserDetails的一个实现。

  • 下面我们自己实现一下UserDetailsService接口
package com.imooc.security.browser;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class MyUserDetailsService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        //模拟从数据库中获取用户信息
        log.info("获取用户名:{} 的认证信息", username);

        return new User(username, "123456", AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

username为前端传递过来的用户名,我们从数据库中查询出这个用户的权限信息,

AuthorityUtils.commaSeparatedStringToAuthorityList("admin")将角色字符串转化成所需的类型

使用User对象封装后进行返回,Spring Security会将返回的真实认证信息与用户输入的认证信息进行比较,如果不相符,

使用SpringSecurity开发基于表单的认证

校验用户:UserDetails

UserDetails接口提供了4个方法,可以由我们自定义实现

方法 说明
isAccountNonExpired 用户是否未过期,true-未过期
isAccountNonLocked 用户是否未锁定,冻结,true-未冻结
isCredentialsNonExpired 密码是否未过期,true-未过期
isEnabled 用户是否未可用,被删除,true-未被删除

关于isAccountNonLockedisEnabled,一般在业务上,一个表示冻结,一个表示删除。 其实都只是两个状态,删除也只是打一个标记位,一般不会真的删除。 被冻结的一般可以被恢复,删除的就不恢复了。

在我们自己实现UserDetails的时候,如果上述四个方法返回false,就表示不通过

如果我们对用户的状态有这种需求,就可以自定义UserDetails的实现

密码处理:PasswordEncoder

我们不会把密码的明文存到数据库中

package org.springframework.security.crypto.password;
public interface PasswordEncoder {

	String encode(CharSequence rawPassword);

	boolean matches(CharSequence rawPassword, String encodedPassword);

}
方法 说明
encode 加密,用户注册入库的时候会调用
matches 判断密码是否正确

一般在新增用户的时候,调用encode,将用户的密码加密后入库

matches一般由Spring Security进行调用 把我们返回的 UserDetails对象中的密码与本次用户输入的进行比较

在上述中,我们使用123456作为密码返回,这个不是密文,用户登陆的时候输入的也是123456,竟然可以通过。 这是因为我们当前还没配置过PasswordEncoder,我们配置一个Spring Security为我们提供的PasswordEncoder

@Bean
public PasswordEncoder passwordEncoder(){
    return new BCryptPasswordEncoder();
}

此时,我们在UserDetails对象中的密码就必须是用BCryptPasswordEncoder加密过的密文才行了

SpringSecurity将用户输入的密码,与返回的加密密码,通过调用matches方法进行校验。

自定义用户认证流程

  • 自定义登陆页面
  • 自定义登陆成功处理

默认是跳转请求的url

  • 自定义登陆失败处理

默认是显示错误信息

自定义登陆页

  • 编写html

browser项目中编写自定义登陆页面

使用SpringSecurity开发基于表单的认证

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
<h2>标准登录页面</h2>
<h3>表单登录</h3>
<form action="/authentication/form" method="post">
    <table>
        <tr>
            <td>用户名:</td>
            <td><input type="text" name="username"></td>
        </tr>
        <tr>
            <td>密码:</td>
            <td><input type="password" name="password"></td>
        </tr>
        <tr>
            <td colspan="2">
                <button type="submit">登录</button>
            </td>
        </tr>
    </table>
</form>

</body>
</html>
  • 修改BrowserSecurityConfig配置文件
package com.imooc.security.browser;

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.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * @Author sherry
 * @Description
 * @Date Create in 2019-03-16
 * @Modified By:
 */
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

//        设置认证方式
        http.formLogin()
//                检测到未登录,跳转到登陆页面
                .loginPage("/imooc-signIn.html")
//                配置处理登陆请求的url,即登陆页面提交的请求地址
                .loginProcessingUrl("/authentication/form")
                .and()
//                配置认证请求方式
                .authorizeRequests()
//                跳转到登陆页面的请求不需要认证
                .antMatchers("/imooc-signIn.html").permitAll()
//                所有的请求
                .anyRequest()
//                都需要身份认证
                .authenticated()
//        忽略CSRF认证,是Spring Boot Security默认配置的一个安全项(跨站请求防护),暂时关闭
                .and()
                .csrf().disable();

    }
}

此时我们再访问接口,发现就是跳转到我们的自定义认证页面了

使用SpringSecurity开发基于表单的认证

这里有一个注意点,一开始的时候,页面跳转的时候报了个404错。原因在于html页面并没有被打包。

修改根pom的resources配置,添加<include>**/*.html</include>即可

自定义任务页面完整代码

默认SpringSecurity处理的表单登陆请求为/login,在这个自定义页面中,我使用了自定义的/authentication/form,所以需要在BrowserSecurityConfig中进行配置.loginProcessingUrl("/authentication/form")

处理不同类型的请求:html or json

使用SpringSecurity开发基于表单的认证

目前,当我们访问API,检测到需要认证,自动跳转到了一个html登陆页面,那么如果是一个app来访问我们呢?这么做就不是那么合理了。所以,我们下面做一个功能,通过用户的请求不同,决定跳转到html登陆页,还是访问json信息。

自定义Controller

编写一个自定义Controller,判断是请求认证失败后,按照html处理还是按照json处理

package com.imooc.security.browser;

import com.imooc.security.browser.support.SimpleResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


@RestController
@Slf4j
public class BrowserSecurityController {
    //请求的缓存对象
    private RequestCache requestCache = new HttpSessionRequestCache();
    //跳转工具类
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    /**
     * 当需要身份认证时,跳转到此请求
     *
     * @param request
     * @param response
     * @return
     */
    @RequestMapping("/authentication/require")
    @ResponseStatus(code = HttpStatus.UNAUTHORIZED)//返回401状态码,表示未授权
    public SimpleResponse requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
        SavedRequest savedRequest = requestCache.getRequest(request, response);
        if (savedRequest != null) {
            String target = savedRequest.getRedirectUrl();//引发跳转的url
            log.info("引发跳转的URL:{}", target);
            if (StringUtils.endsWithIgnoreCase(target, ".html")) {//如果引发跳转的url后缀为html,则跳转到html登陆页面
                //跳转到自定义配置的登陆页面
                redirectStrategy.sendRedirect(request, response, "/imooc-signIn.html");
            }
        }
        return new SimpleResponse("访问的服务需要身份认证");
    }
}

修改配置类

package com.imooc.security.browser;

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.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;


@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

//        设置认证方式
        http.formLogin()
//                检测到未登录,跳转到登陆页面  或  处理跳转的Controller地址
                .loginPage("/authentication/require")
//                配置处理登陆请求的url,即登陆页面提交的请求地址;默认为 /login
                .loginProcessingUrl("/authentication/form")
                .and()
//                配置认证请求方式
                .authorizeRequests()
//                跳转到登陆页面的请求不需要认证
                .antMatchers("/imooc-signIn.html", "/authentication/require").permitAll()
//                所有的请求
                .anyRequest()
//                都需要身份认证
                .authenticated()
//        忽略CSRF认证,是Spring Boot Security默认配置的一个安全项(跨站请求防护),暂时关闭
                .and()
                .csrf().disable();

    }
}

重构代码:系统配置项

对于一些可以灵活变化的信息,我们把它抽取到配置类中。如:认证失败后的登录页地址

配置类的编写一般遵循这些个原则

1、一个系统中不要有太多的配置类入口

2、java中提供默认配置

3、调用方的配置中间中可以覆盖2中的配置

4、请求的url参数可以覆盖3中的配置

这么做的话,我们的系统配置就非常灵活了

  • BrowserProperties
package com.imooc.security.core.properties;

public class BrowserProperties {

    private String loginPage = "/imooc-signIn.html";

    public String getLoginPage() {
        return loginPage;
    }

    public void setLoginPage(String loginPage) {
        this.loginPage = loginPage;
    }
}
  • SecurityProperties
package com.imooc.security.core.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;


@ConfigurationProperties(prefix = "imooc.security")
public class SecurityProperties {
    private BrowserProperties browser = new BrowserProperties();

    public BrowserProperties getBrowser() {
        return browser;
    }

    public void setBrowser(BrowserProperties browser) {
        this.browser = browser;
    }
}
  • SecurityCoreConfig
package com.imooc.security.core;

import com.imooc.security.core.properties.SecurityProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;


@Configuration
@EnableConfigurationProperties(SecurityProperties.class)//使配置文件生效
public class SecurityCoreConfig {
}

然后我们就可以修改我们的代码了

使用SpringSecurity开发基于表单的认证

使用SpringSecurity开发基于表单的认证

demo中配置

imooc:
  security:
    browser:
      loginPage: /imooc-demo.html

访问:http://localhost:8080/hello.html,发现页面被跳转到了我们配置的页面上,而不是原先的imooc-signIn.html页面

认证成功、失败自定义处理

默认情况下,登陆成功后,去访问原先请求的URL。但在实际场景中,我们在登陆成功后,往往需要做很多操作,如登陆日志的记录等等。

默认情况下,登陆失败后,是在登陆页面显示错误信息。如果我们还想有些额外的操作,该如何处理呢?

AuthenticationSuccessHandler

自定义认证成功处理类

package com.imooc.security.browser.auth;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.imooc.security.core.properties.SecurityProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


@Component("imoocAuthSuccessHandler")
@Slf4j
public class ImoocAuthSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {

        log.info("登陆成功");

        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);

        response.getWriter().write(objectMapper.writeValueAsString(authentication));
    }
}

登陆认证成功后,我们往浏览器输出当前认证用户对象信息,如下:

使用SpringSecurity开发基于表单的认证

"authenticated": true,:当前信息已经经过了身份认证

"authorities": [
{
"authority": "admin"
}
],

用户拥有的权限,或者说是角色

"credentials": null,:用户密码,但一般被SpringSecurity处理后会隐藏起来

"details": {
	"remoteAddress": "0:0:0:0:0:0:0:1",
	"sessionId": "C86D521C7A80B54DB39B1B5FD135EED5"
},

认证请求的客户端信息

"name": "fdfd":认证请求的用户名

"principal": {
"password": null,
"username": "fdfd",
"authorities": [
{
"authority": "admin"
}
],
"accountNonExpired": true,
"accountNonLocked": true,
"credentialsNonExpired": true,
"enabled": true
},

我们自定义的UserDetails中的内容

优化

我们不再实现接口,而是继承SavedRequestAwareAuthenticationSuccessHandler,这个是SpringSecurity默认的成功处理器

package com.imooc.security.browser.auth;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.imooc.security.core.properties.LoginType;
import com.imooc.security.core.properties.SecurityProperties;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.builder.ReflectionToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


@Component("imoocAuthSuccessHandler")
@Slf4j
public class ImoocAuthSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private SecurityProperties securityProperties;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {

        log.info("登陆成功:{}", securityProperties.getBrowser().getLoginType());

        if (LoginType.JSON == securityProperties.getBrowser().getLoginType()) {//如果当前系统配置的是json请求
            response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
//            response.getWriter().write(objectMapper.writeValueAsString(authentication));
            log.info(ReflectionToStringBuilder.toString(authentication, ToStringStyle.MULTI_LINE_STYLE));
            //然后按照默认处理,继续调用请求的接口
            super.onAuthenticationSuccess(request, response, authentication);
        } else {//如果当前系统配置的不是json,则按照默认处理,默认处理请求的接口
            //            redirectStrategy.sendRedirect(request,response,"/index1.html");//跳转到自定义的页面上
            super.onAuthenticationSuccess(request, response, authentication);
        }


    }
}

AuthenticationFailureHandler

自定义认证失败处理类

package com.imooc.security.browser.auth;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.imooc.security.browser.support.SimpleResponse;
import com.imooc.security.core.properties.LoginType;
import com.imooc.security.core.properties.SecurityProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Slf4j
@Component("/imoocAuthFailureHandler")
public class ImoocAuthFailureHandler extends SimpleUrlAuthenticationFailureHandler {
    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private SecurityProperties securityProperties;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        log.info("登陆失败:{}", securityProperties.getBrowser().getLoginType());

        if (LoginType.JSON == securityProperties.getBrowser().getLoginType()) {
            response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            response.getWriter().write(objectMapper.writeValueAsString(new SimpleResponse(e.getMessage())));
        } else {
            //默认跳转到登陆认证页
            super.onAuthenticationFailure(request, response, e);
        }
    }
}

配置

我们的成功、失败处理器,只有配置后才会起作用

使用SpringSecurity开发基于表单的认证

获取认证用户的信息

系统中往往会有这种需求,就是需要知道当前访问的人是谁

  • 从线程中获取
@GetMapping("/me")
public Object getCurrentUser(){
    return SecurityContextHolder.getContext().getAuthentication();
}
  • SpringMVC自动注入
@GetMapping("/me1")
public Object getCurrentUser(Authentication authentication){
    return authentication;
}

深入理解认证流程

认证流程

使用SpringSecurity开发基于表单的认证

认证结果如何在多个请求间共享

使用SpringSecurity开发基于表单的认证

SecurityContextPersistenceFilter

进来的时候检查session中是否有认证信息,有,放到请求线程中;

出去的时候检查线程中是否有认证信息,有,放到session

图形验证码

  • 根据随机数生成图片
  • 将随机数保存到session中
  • 将生成的图片写到接口响应中

图形验证码对象

package com.imooc.security.core.validate.code;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.awt.image.BufferedImage;
import java.time.LocalDateTime;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ImageCode {
    //    图片
    private BufferedImage image;
    //    随机数
    private String code;
    //    过期时间点
    private LocalDateTime expireTime;

    /**
     * @param image
     * @param code
     * @param expireIn 有效时间,秒
     */
    public ImageCode(BufferedImage image, String code, int expireIn) {
        this.image = image;
        this.code = code;
        this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
    }
}

生成图形验证码的接口

package com.imooc.security.core.validate.code;

import lombok.extern.slf4j.Slf4j;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;

import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;


@RestController
@RequestMapping("/code")
@Slf4j
public class ValidateCodeController {

    public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";

//    操作Session的Spring工具类
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    /**
     * 验证码生成,并将验证码存储在session中
     * @param request
     * @param response
     * @throws IOException
     */
    @GetMapping("/image")
    public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        ImageCode imageCode = createImageCode(request);
        sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode);
        ServletOutputStream sos = response.getOutputStream();
        ImageIO.write(imageCode.getImage(), "JPEG", sos);
        sos.close();
    }

    /**
     * 生成图形验证码
     *
     * @param request
     * @return
     */
    private ImageCode createImageCode(HttpServletRequest request) {
        int width = 67;
        int height = 23;
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

        Graphics g = image.getGraphics();

        Random random = new Random();

        g.setColor(getRandColor(200, 250));
        g.fillRect(0, 0, width, height);
        g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
        g.setColor(getRandColor(160, 200));
        for (int i = 0; i < 155; i++) {
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            int xl = random.nextInt(12);
            int yl = random.nextInt(12);
            g.drawLine(x, y, x + xl, y + yl);
        }

        String sRand = "";
        for (int i = 0; i < 4; i++) {
            String rand = String.valueOf(random.nextInt(10));
            sRand += rand;
            g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
            g.drawString(rand, 13 * i + 6, 16);
        }

        g.dispose();

        return new ImageCode(image, sRand, 60);
    }

    /**
     * 生成随机背景条纹
     *
     * @param fc
     * @param bc
     * @return
     */
    private Color getRandColor(int fc, int bc) {
        Random random = new Random();
        if (fc > 255) {
            fc = 255;
        }
        if (bc > 255) {
            bc = 255;
        }
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return new Color(r, g, b);
    }
}

过滤器校验

package com.imooc.security.core.validate.code;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


@Slf4j
public class ValidateCodeFilter extends OncePerRequestFilter {

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    private AuthenticationFailureHandler authenticationFailureHandler;

    @Override
    protected void doFilterInternal(HttpServletRequest request
                                    , HttpServletResponse response
                                    , FilterChain filterChain) throws ServletException, IOException {
//只有是登陆请求的情况下才进行验证码校验
        if (StringUtils.equals("/authentication/form", request.getRequestURI())
                && StringUtils.equalsIgnoreCase("post", request.getMethod())) {
            try {
                validate(new ServletWebRequest(request));
            } catch (ValidateCodeException e) {
                logger.error(e.getMessage(), e);
                authenticationFailureHandler.onAuthenticationFailure(request, response, e);
//                抛出异常后不继续Filter,直接返回掉,切记
                return;
            }

        }
        filterChain.doFilter(request, response);
    }

    private void validate(ServletWebRequest request) {


        ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY);
        String codeInRequest = "";
        try {
            codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode");
        } catch (ServletRequestBindingException e) {
            throw new ValidateCodeException("获取验证码的值失败");
        }

        if (StringUtils.isBlank(codeInRequest)) {
            throw new ValidateCodeException("验证码的值不能为空");
        }

        if (codeInSession == null) {
            throw new ValidateCodeException("验证码不存在");
        }

        if (codeInSession.isExpried()) {
            sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
            throw new ValidateCodeException("验证码已过期");
        }

        if (!StringUtils.equals(codeInSession.getCode(), codeInRequest)) {
            throw new ValidateCodeException("验证码不匹配");
        }

        sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
    }

    public AuthenticationFailureHandler getAuthenticationFailureHandler() {
        return authenticationFailureHandler;
    }

    public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
        this.authenticationFailureHandler = authenticationFailureHandler;
    }
}

OncePerRequestFilter:Spring提供的工具类,保证过滤器仅被调用一次

自己封装的异常接口

package com.imooc.security.core.validate.code;

import org.springframework.security.core.AuthenticationException;



public class ValidateCodeException extends AuthenticationException {
    public ValidateCodeException(String message) {
        super(message);
    }
}

配置

package com.imooc.security.browser;

import com.imooc.security.browser.auth.ImoocAuthFailureHandler;
import com.imooc.security.browser.auth.ImoocAuthSuccessHandler;
import com.imooc.security.core.properties.SecurityProperties;
import com.imooc.security.core.validate.code.ValidateCodeFilter;
import org.springframework.beans.factory.annotation.Autowired;
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.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private ImoocAuthSuccessHandler imoocAuthSuccessHandler;
    @Autowired
    private ImoocAuthFailureHandler imoocAuthFailureHandler;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
        validateCodeFilter.setAuthenticationFailureHandler(imoocAuthFailureHandler);

//        在UsernamePasswordAuthenticationFilter前添加自定义过滤器
        http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()
//                检测到未登录,跳转到登陆页面  或  处理跳转的Controller地址
                .loginPage("/authentication/require")
//                配置处理登陆请求的url,即登陆页面提交的请求地址;默认为 /login
                .loginProcessingUrl("/authentication/form")
                .successHandler(imoocAuthSuccessHandler)
                .failureHandler(imoocAuthFailureHandler)
                .and()
//                配置认证请求方式
                .authorizeRequests()
//                跳转到登陆页面的请求不需要认证
                .antMatchers(securityProperties.getBrowser().getLoginPage()
                        , "/authentication/require"
                        , "/code/image").permitAll()
//                所有的请求
                .anyRequest()
//                都需要身份认证
                .authenticated()
//        忽略CSRF认证,是Spring Boot Security默认配置的一个安全项(跨站请求防护),暂时关闭
                .and()
                .csrf().disable();

    }
}


  • 完成代码

完整代码

图像验证码代码优化

  • 验证码的基本参数可配:图片大小、验证码长度、验证码有效时间
  • 验证码拦截的接口可配置:即哪些接口需要执行验证码的拦截逻辑
  • 验证码的生成逻辑可配

验证码基本参数配置

使用SpringSecurity开发基于表单的认证

  • ImageCodeProperties
package com.imooc.security.core.properties;

import lombok.Data;

/**
 * @Author sherry
 * @Description
 * @Date Create in 2019-03-20
 * @Modified By:
 */
@Data
public class ImageCodeProperties {
    private int width = 67;
    private int height = 23;
    private int length = 4;
    private int expireIn = 60;
}

  • ValidateCodeProperties
package com.imooc.security.core.properties;

import lombok.Data;

/**
 * @Author sherry
 * @Description
 * @Date Create in 2019-03-20
 * @Modified By:
 */

@Data
public class ValidateCodeProperties {
    private ImageCodeProperties image = new ImageCodeProperties();
}

  • SecurityProperties
package com.imooc.security.core.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;


@ConfigurationProperties(prefix = "imooc.security")
@Data
public class SecurityProperties {
    private BrowserProperties browser = new BrowserProperties();

    private ValidateCodeProperties code = new ValidateCodeProperties();

}
  • SecurityCoreConfig
package com.imooc.security.core;

import com.imooc.security.core.properties.SecurityProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
 * @Author 张柳宁
 * @Description
 * @Date Create in 2018/3/24
 * @Modified By:
 */
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)//使配置文件生效
public class SecurityCoreConfig {
}
  • 应用级配置:在demo项目的yml文件中配置
imooc:
  security:
    browser:
      loginPage: /imooc-demo.html
      loginType: REDIRECT
    code:
      image:
        length: 6
        width: 200
        height: 20
  • 请求级配置:imooc-demo.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>登录-demo</title>
</head>
<body>
<h2>标准登录页面</h2>
<h3>表单登录</h3>
<form action="/authentication/form" method="post">
    <table>
        <tr>
            <td>用户名:</td>
            <td><input type="text" name="username"></td>
        </tr>
        <tr>
            <td>密码:</td>
            <td><input type="password" name="password"></td>
        </tr>
        <tr>
            <td>图形验证码:</td>
            <td>
                <input type="text" name="imageCode">
                <img src="/code/image?width=200&height=50">
            </td>
        </tr>
        <tr>
            <td colspan="2">
                <button type="submit">登录</button>
            </td>
        </tr>
    </table>
</form>

</body>
</html>
  • ValidateCodeController
package com.imooc.security.core.validate.code;

import com.imooc.security.core.properties.SecurityProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;

import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;

/**
 * @Author sherry
 * @Description
 * @Date Create in 2019-03-18
 * @Modified By:
 */
@RestController
@RequestMapping("/code")
@Slf4j

public class ValidateCodeController {

    @Autowired
    private SecurityProperties securityProperties;

    public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";

    //    操作Session的Spring工具类
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    /**
     * 验证码生成,并将验证码存储在session中
     *
     * @param request
     * @param response
     * @throws IOException
     */
    @GetMapping("/image")
    public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        ImageCode imageCode = createImageCode(request);
        sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode);
        ServletOutputStream sos = response.getOutputStream();
        ImageIO.write(imageCode.getImage(), "JPEG", sos);
        sos.close();
    }

    /**
     * 生成图形验证码
     *
     * @param request
     * @return
     */
    private ImageCode createImageCode(HttpServletRequest request) {
        int width = ServletRequestUtils.getIntParameter(request,"width",securityProperties.getCode().getImage().getWidth());
        int height = ServletRequestUtils.getIntParameter(request,"height",securityProperties.getCode().getImage().getHeight());
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

        Graphics g = image.getGraphics();

        Random random = new Random();

        g.setColor(getRandColor(200, 250));
        g.fillRect(0, 0, width, height);
        g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
        g.setColor(getRandColor(160, 200));
        for (int i = 0; i < 155; i++) {
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            int xl = random.nextInt(12);
            int yl = random.nextInt(12);
            g.drawLine(x, y, x + xl, y + yl);
        }

        String sRand = "";
        for (int i = 0; i < securityProperties.getCode().getImage().getLength(); i++) {
            String rand = String.valueOf(random.nextInt(10));
            sRand += rand;
            g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
            g.drawString(rand, 13 * i + 6, 16);
        }

        g.dispose();

        log.info("生成验证码:{}",sRand);

        return new ImageCode(image, sRand, securityProperties.getCode().getImage().getExpireIn());
    }

    /**
     * 生成随机背景条纹
     *
     * @param fc
     * @param bc
     * @return
     */
    private Color getRandColor(int fc, int bc) {
        Random random = new Random();
        if (fc > 255) {
            fc = 255;
        }
        if (bc > 255) {
            bc = 255;
        }
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return new Color(r, g, b);
    }
}

验证码拦截接口配置

  • 配置文件
imooc:
  security:
    browser:
      loginPage: /imooc-demo.html
      loginType: REDIRECT
    code:
      image:
        length: 6
        width: 200
        height: 20
        urls:
          - /authentication/form
          - /user
          - /user/*
@Data
public class ImageCodeProperties {
    private int width = 67;
    private int height = 23;
    private int length = 4;
    private int expireIn = 60;
    //需要验证码校验的接口
    private List<String> urls;
}
  • 过滤器修改
package com.imooc.security.core.validate.code;

import com.imooc.security.core.properties.SecurityProperties;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

/**
 * @Author sherry
 * @Description
 * @Date Create in 2019-03-18
 * @Modified By:
 */
@Slf4j
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    private AuthenticationFailureHandler authenticationFailureHandler;

    private SecurityProperties securityProperties;

    private AntPathMatcher antPathMatcher;

    private Set<String> urls;

    @Override
    public void afterPropertiesSet() throws ServletException {
        urls = new HashSet<>(securityProperties.getCode().getImage().getUrls());
    }


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        boolean action = false;
        for (String url : urls) {
            if (antPathMatcher.match(url, request.getRequestURI())) {
                log.info("对:{} 执行图像验证码校验", request.getRequestURI());
                action = true;
                break;
            }
        }

        if (action) {
            try {
                validate(new ServletWebRequest(request));
            } catch (ValidateCodeException e) {
                logger.error(e.getMessage(), e);
                authenticationFailureHandler.onAuthenticationFailure(request, response, e);
//                抛出异常后不继续Filter,直接返回掉
                return;
            }

        }


        filterChain.doFilter(request, response);
    }

    private void validate(ServletWebRequest request) {


        ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY);
        String codeInRequest = "";
        try {
            codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode");
        } catch (ServletRequestBindingException e) {
            throw new ValidateCodeException("获取验证码的值失败");
        }

        if (StringUtils.isBlank(codeInRequest)) {
            throw new ValidateCodeException("验证码的值不能为空");
        }

        if (codeInSession == null) {
            throw new ValidateCodeException("验证码不存在");
        }

        if (codeInSession.isExpried()) {
            sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
            throw new ValidateCodeException("验证码已过期");
        }

        if (!StringUtils.equals(codeInSession.getCode(), codeInRequest)) {
            throw new ValidateCodeException("验证码不匹配");
        }

        sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
    }

    public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
        this.authenticationFailureHandler = authenticationFailureHandler;
    }

    public void setSecurityProperties(SecurityProperties securityProperties) {
        this.securityProperties = securityProperties;
    }

    public void setAntPathMatcher(AntPathMatcher antPathMatcher) {
        this.antPathMatcher = antPathMatcher;
    }
}

  • 配置文件修改
package com.imooc.security.browser;

import com.imooc.security.browser.auth.ImoocAuthFailureHandler;
import com.imooc.security.browser.auth.ImoocAuthSuccessHandler;
import com.imooc.security.core.properties.SecurityProperties;
import com.imooc.security.core.validate.code.ValidateCodeFilter;
import org.springframework.beans.factory.annotation.Autowired;
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.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.AntPathMatcher;

/**
 * @Author sherry
 * @Description
 * @Date Create in 2019-03-16
 * @Modified By:
 */
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private ImoocAuthSuccessHandler imoocAuthSuccessHandler;
    @Autowired
    private ImoocAuthFailureHandler imoocAuthFailureHandler;

    @Autowired
    private AntPathMatcher antPathMatcher;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


    @Bean
    public AntPathMatcher antPathMatcher() {
        return new AntPathMatcher();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
        validateCodeFilter.setAuthenticationFailureHandler(imoocAuthFailureHandler);
        validateCodeFilter.setSecurityProperties(securityProperties);
        validateCodeFilter.setAntPathMatcher(antPathMatcher);
//        调用初始化方法
        validateCodeFilter.afterPropertiesSet();

//        在UsernamePasswordAuthenticationFilter前添加自定义过滤器
        http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()
//                检测到未登录,跳转到登陆页面  或  处理跳转的Controller地址
                .loginPage("/authentication/require")
//                配置处理登陆请求的url,即登陆页面提交的请求地址;默认为 /login
                .loginProcessingUrl("/authentication/form")
                .successHandler(imoocAuthSuccessHandler)
                .failureHandler(imoocAuthFailureHandler)
                .and()
//                配置认证请求方式
                .authorizeRequests()
//                跳转到登陆页面的请求不需要认证
                .antMatchers(securityProperties.getBrowser().getLoginPage()
                        , "/authentication/require"
                        , "/code/image").permitAll()
//                所有的请求
                .anyRequest()
//                都需要身份认证
                .authenticated()
//        忽略CSRF认证,是Spring Boot Security默认配置的一个安全项(跨站请求防护),暂时关闭
                .and()
                .csrf().disable();
//        设置认证方式

    }
}

验证码生成逻辑配置

所谓验证码生成逻辑可配置,就是说,提供接口,供使用方进行实现

  • 接口
package com.imooc.security.core.validate.code;


import javax.servlet.ServletRequest;


public interface ValidateCodeGenerator {
    ImageCode generate(ServletRequest request);
}

  • 默认实现
package com.imooc.security.core.validate.code;

import com.imooc.security.core.properties.SecurityProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.ServletRequestUtils;

import javax.servlet.ServletRequest;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;


@Slf4j
public class ImageCodeGenerator implements ValidateCodeGenerator {

    private SecurityProperties securityProperties;

    @Override
    public ImageCode generate(ServletRequest request) {
        int width = ServletRequestUtils.getIntParameter(request, "width", securityProperties.getCode().getImage().getWidth());
        int height = ServletRequestUtils.getIntParameter(request, "height", securityProperties.getCode().getImage().getHeight());
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

        Graphics g = image.getGraphics();

        Random random = new Random();

        g.setColor(getRandColor(200, 250));
        g.fillRect(0, 0, width, height);
        g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
        g.setColor(getRandColor(160, 200));
        for (int i = 0; i < 155; i++) {
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            int xl = random.nextInt(12);
            int yl = random.nextInt(12);
            g.drawLine(x, y, x + xl, y + yl);
        }

        String sRand = "";
        for (int i = 0; i < securityProperties.getCode().getImage().getLength(); i++) {
            String rand = String.valueOf(random.nextInt(10));
            sRand += rand;
            g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
            g.drawString(rand, 13 * i + 6, 16);
        }

        g.dispose();

        log.info("生成验证码:{}", sRand);

        return new ImageCode(image, sRand, securityProperties.getCode().getImage().getExpireIn());

    }

    private Color getRandColor(int fc, int bc) {
        Random random = new Random();
        if (fc > 255) {
            fc = 255;
        }
        if (bc > 255) {
            bc = 255;
        }
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return new Color(r, g, b);
    }

    public void setSecurityProperties(SecurityProperties securityProperties) {
        this.securityProperties = securityProperties;
    }
}

  • Bean配置
package com.imooc.security.core.validate.code;

import com.imooc.security.core.properties.SecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Author sherry
 * @Description
 * @Date Create in 2019-03-20
 * @Modified By:
 */
@Configuration
public class ValidateCodeBeanConfig {

    @Autowired
    private SecurityProperties securityProperties;

    @Bean
//    如果用户没有实现接口,且名称为 imageCodeGenerator,则使用系统默认提供的
    @ConditionalOnMissingBean(name = "imageCodeGenerator")
    public ValidateCodeGenerator imageCodeGenerator() {
        ValidateCodeGenerator codeGenerator = new ImageCodeGenerator();
        ((ImageCodeGenerator) codeGenerator).setSecurityProperties(securityProperties);
        return codeGenerator;
    }

}

  • Controller使用修改
package com.imooc.security.core.validate.code;

import com.imooc.security.core.properties.SecurityProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;

import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


@RestController
@RequestMapping("/code")
@Slf4j
public class ValidateCodeController {

    public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";

    //    操作Session的Spring工具类
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Autowired
    private ValidateCodeGenerator imageCodeGenerator;

    /**
     * 验证码生成,并将验证码存储在session中
     *
     * @param request
     * @param response
     * @throws IOException
     */
    @GetMapping("/image")
    public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        ImageCode imageCode = imageCodeGenerator.generate(request);
        sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode);
        ServletOutputStream sos = response.getOutputStream();
        ImageIO.write(imageCode.getImage(), "JPEG", sos);
        sos.close();
    }
}

如果我们自定义了一个bean名称为imageCodeGenerator的类,就会替换掉系统的默认配置

形如

@Component("imageCodeGenerator")
public MyImageCodeGenerator implements ValidateCodeGenerator {
  ...
}

这里体现了一个开发思想,就是以增量的方式与适应变化。就是说当需求、逻辑发生变动的时候,是增加代码,而不是去修改原先的代码

完整代码

记住我

这个也是身份认证的一个常见特性

即:用户登录认证成功后,在一段时间内不需要重复认证

原理

使用SpringSecurity开发基于表单的认证

1、首次登陆后,cookie和用户信息会存到数据库
2、下一次登陆的时候,经过 RememberMeAuthenticationFilter ,拿着cookie去数据库查询用户
3、用2中返回的用户自动执行认认证逻辑

RememberMeAuthenticationFilter是最后一个绿色的过滤器,当所有过滤器都没法认证的时候,就使用RememberMeAuthenticationFilter进行认证。

使用SpringSecurity开发基于表单的认证

记住我功能的实现

  • 前端
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>登录-demo</title>
</head>
<body>
<h2>标准登录页面</h2>
<h3>表单登录</h3>
<form action="/authentication/form" method="post">
    <table>
        <tr>
            <td>用户名:</td>
            <td><input type="text" name="username"></td>
        </tr>
        <tr>
            <td>密码:</td>
            <td><input type="password" name="password"></td>
        </tr>
        <tr>
            <td>图形验证码:</td>
            <td>
                <input type="text" name="imageCode">
                <img src="/code/image">
                <!--<img src="/code/image?width=200&height=50">-->
            </td>
        </tr>
        <tr>
            <!--name名称是remember-me,固定不可改-->
            <td colspan='2'><input name="remember-me" type="checkbox" value="true"/>记住我</td>
        </tr>
        <tr>
            <td colspan="2">
                <button type="submit">登录</button>
            </td>
        </tr>
    </table>
</form>

</body>
</html>
  • 配置PersistentTokenRepository

BrowserSecurityConfig中添加配置

    @Autowired
    private DataSource dataSource;

    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        //启动的时候就初始化表,注意,就在第一次启动的时候执行,以后要注释掉
        tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }

待会儿修改配置的时候要用到这个Bean

  • 配置UserDetailsService
    @Autowired
    private UserDetailsService userDetailsService;

也是在BrowserSecurityConfig中添加配置

  • BrowserSecurityConfig中整合各个组件
package com.imooc.security.browser;

import com.imooc.security.browser.auth.ImoocAuthFailureHandler;
import com.imooc.security.browser.auth.ImoocAuthSuccessHandler;
import com.imooc.security.core.properties.SecurityProperties;
import com.imooc.security.core.validate.code.ValidateCodeFilter;
import org.springframework.beans.factory.annotation.Autowired;
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.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.util.AntPathMatcher;

import javax.sql.DataSource;

/**
 * @Author sherry
 * @Description
 * @Date Create in 2019-03-16
 * @Modified By:
 */
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private ImoocAuthSuccessHandler imoocAuthSuccessHandler;
    @Autowired
    private ImoocAuthFailureHandler imoocAuthFailureHandler;

    @Autowired
    private AntPathMatcher antPathMatcher;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


    @Bean
    public AntPathMatcher antPathMatcher() {
        return new AntPathMatcher();
    }

    @Autowired
    private DataSource dataSource;

    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        //启动的时候就初始化表,注意,就在第一次启动的时候执行,以后要注释掉
        tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
        validateCodeFilter.setAuthenticationFailureHandler(imoocAuthFailureHandler);
        validateCodeFilter.setSecurityProperties(securityProperties);
        validateCodeFilter.setAntPathMatcher(antPathMatcher);
//        调用初始化方法
        validateCodeFilter.afterPropertiesSet();

//        在UsernamePasswordAuthenticationFilter前添加自定义过滤器
        http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()
//                检测到未登录,跳转到登陆页面  或  处理跳转的Controller地址
                .loginPage("/authentication/require")
//                配置处理登陆请求的url,即登陆页面提交的请求地址;默认为 /login
                .loginProcessingUrl("/authentication/form")
                .successHandler(imoocAuthSuccessHandler)
                .failureHandler(imoocAuthFailureHandler)
                .and()
                    .rememberMe()
                    .tokenRepository(persistentTokenRepository())
                    .tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())
                    .userDetailsService(userDetailsService)
                .and()
//                配置认证请求方式
                    .authorizeRequests()
    //                跳转到登陆页面的请求不需要认证
                    .antMatchers(securityProperties.getBrowser().getLoginPage()
                            , "/authentication/require"
                            , "/code/image").permitAll()
    //                所有的请求
                    .anyRequest()
    //                都需要身份认证
                    .authenticated()
//        忽略CSRF认证,是Spring Boot Security默认配置的一个安全项(跨站请求防护),暂时关闭
                .and()
                .csrf().disable();
//        设置认证方式

    }
}

使用SpringSecurity开发基于表单的认证

登陆认证后,数据库中会创建persistent_logins表,并添加一条认证记录。
然后即使关闭服务,重新启动,访问需要认证的服务,也不需要认证,因为会通过数据库中的认证信息自动认证。

一般我们是自己创建表结构

create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, "
			+ "token varchar(64) not null, last_used timestamp not null)

使用SpringSecurity开发基于表单的认证

手机号+短信认证

除了用户名密码登录外,手机号+短信验证码登录也是一种常见的登陆方式

用户名密码登陆过程中,由SpringSecurity提供的过滤器实现认证,手机号+短信的认证方式中,我们只验证手机,验证码在更前面的验证码过滤器中进行校验。

  • 开发短信发送接口
  • 校验短信验证码并登陆
  • 代码重构

整合验证码代码重构

我们已经开发了一个图形验证码接口,我们就在此基础上进行开发

其实逻辑和开发图形验证码是类似的

1、生成

2、存入session

3、告知使用者

像这种主干逻辑相同,但是步骤实现不同,我们可以使用模板方法的模式抽象出来

使用SpringSecurity开发基于表单的认证

详细代码

ValidateCodeController作为入口来看

具体逻辑以代码为主

主要是代码重构的技巧,不是SpringSecurity的内容,这里就不赘述了

短信校验配置

使用SpringSecurity开发基于表单的认证

不同的认证方式需要由不同的过滤器来处理,对于用户名密码形式的表单认证,系统已经提供了对应的过滤器。

下面,我们就需要根据仿照这个流程,对短信验证码的登陆形式进行一次认证。实现一些自定义的类

注意,本流程只是对手机号进行校验,不是对手机号和短信验证码进行校验。。 短信验证码的校验放在更前面的过滤器中。走一遍这个流程,是为了让用户处于已认证状态。之所以短信验证码单独提取出来,是因为短信验证码、图形验证码,这个都是比较通用的功能,在系统的好多地方都能够用到。

SmsCodeAuthenticationToken

SmsCodeAuthenticationToken是用户身份对象

package com.imooc.security.core.auth.mobile;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = 420L;
    private final Object principal;

    public SmsCodeAuthenticationToken(String mobile) {
        super((Collection)null);
        this.principal = mobile;
        this.setAuthenticated(false);
    }

    public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true);
    }

    public Object getCredentials() {
        return null;
    }

    public Object getPrincipal() {
        return this.principal;
    }

    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if(isAuthenticated) {
            throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        } else {
            super.setAuthenticated(false);
        }
    }

    public void eraseCredentials() {
        super.eraseCredentials();
    }
}

这个类参考UsernamePasswordAuthenticationToken编写

SmsCodeAuthenticationFilter

拦截短信验证登录请求,并初始化SmsCodeAuthenticationToken

package com.imooc.security.core.auth.mobile;

import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    public static final String IMOOC_FORM_MOBILE_KEY = "mobile";
    private String mobileParameter = IMOOC_FORM_MOBILE_KEY;
    private boolean postOnly = true;

    public SmsCodeAuthenticationFilter() {
    //校验请求的url与方法类型
        super(new AntPathRequestMatcher("/authentication/mobile", "POST"));
    }

    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 {
            String mobile = this.obtainMobile(request);
            if(mobile == null) {
                mobile = "";
            }

            mobile = mobile.trim();
            SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
            this.setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

    /**
     * 获取手机号的方法
     * @param request
     * @return
     */
    protected String obtainMobile(HttpServletRequest request) {
        return request.getParameter(this.mobileParameter);
    }

    protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }

    public String getMobileParameter() {
        return mobileParameter;
    }

    public void setMobileParameter(String mobileParameter) {
        this.mobileParameter = mobileParameter;
    }
}

SmsCodeAuthenticationProvider

提供校验逻辑

package com.imooc.security.core.auth.mobile;

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;

public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
        // (String) authenticationToken.getPrincipal(),这个获取到的是手机号,通过手机号获取用户信息
        UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());
        if(user==null){
            throw new InternalAuthenticationServiceException("无法获取用户信息");
        }


        //如果还有其他校验逻辑的话,是要加载这里的,如,手机号和短信验证码是否匹配。当然,现在是把这个校验放在了最前面的过滤器中了

//        认证后的 SmsCodeAuthenticationToken
        SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user,user.getAuthorities());
        //把未认证的信息放到已认证的detail里面
        authenticationResult.setDetails(authenticationToken.getDetails());

        return authenticationResult;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }

    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
}

SmsCodeFilter

短信验证码过滤器

package com.imooc.security.core.validate.code;

import com.imooc.security.core.properties.SecurityProperties;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

/**
 * OncePerRequestFilter:Spring提供的工具类,保证过滤器仅被调用一次
 *
 * @Author 张柳宁
 * @Description
 * @Date Create in 2018/3/27
 * @Modified By:
 */
public class SmsCodeFilter extends OncePerRequestFilter implements InitializingBean {

    private Logger logger = LoggerFactory.getLogger(getClass());
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    private AuthenticationFailureHandler authenticationFailureHandler;

    private Set<String> urls = new HashSet<>();

    private SecurityProperties securityProperties;

    //    用于判断Ant形式的字符串匹配规则
    private AntPathMatcher pathMatcher = new AntPathMatcher();

    @Override
    public void afterPropertiesSet() throws ServletException {
        super.afterPropertiesSet();
//        String[] configUrls = StringUtils.splitByWholeSeparator(securityProperties.getCode().getSms().getUrl(), ",");
//        初始化当前配置的需要验证码的url
//        for (String config : configUrls) {
//            urls.add(config);
//        }
        urls.add("/authentication/mobile");//登陆是必须要验证码的
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //只有是登陆请求的情况下才进行验证码校验
        boolean action = false;
        for (String url : urls) {
            if (pathMatcher.match(url, request.getRequestURI())) {
                action = true;
                break;
            }
        }
        if (action) {
            try {
                validate(new ServletWebRequest(request));
            } catch (ValidateCodeException e) {
                logger.error(e.getMessage(), e);
                authenticationFailureHandler.onAuthenticationFailure(request, response, e);
//                抛出异常后不继续Filter,直接返回掉
                return;
            }

        }
        filterChain.doFilter(request, response);
    }

    private void validate(ServletWebRequest request) {


        ValidateCode codeInSession = (ValidateCode) sessionStrategy.getAttribute(request, ValidateCodeProcessor.SESSION_KEY_PREFIX+"SMS");
        String codeInRequest = "";
        try {
            codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "smsCode");
        } catch (ServletRequestBindingException e) {
            throw new ValidateCodeException("获取验证码的值失败");
        }

        if (StringUtils.isBlank(codeInRequest)) {
            throw new ValidateCodeException("验证码的值不能为空");
        }

        if (codeInSession == null) {
            throw new ValidateCodeException("验证码不存在");
        }

        if (codeInSession.isExpired()) {
            sessionStrategy.removeAttribute(request, ValidateCodeProcessor.SESSION_KEY_PREFIX+"SMS");
            throw new ValidateCodeException("验证码已过期");
        }

        if (!StringUtils.equals(codeInSession.getCode(), codeInRequest)) {
            throw new ValidateCodeException("验证码不匹配");
        }

        sessionStrategy.removeAttribute(request, ValidateCodeProcessor.SESSION_KEY_PREFIX+"SMS");
    }

    public AuthenticationFailureHandler getAuthenticationFailureHandler() {
        return authenticationFailureHandler;
    }

    public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
        this.authenticationFailureHandler = authenticationFailureHandler;
    }

    public Set<String> getUrls() {
        return urls;
    }

    public void setUrls(Set<String> urls) {
        this.urls = urls;
    }

    public SecurityProperties getSecurityProperties() {
        return securityProperties;
    }

    public void setSecurityProperties(SecurityProperties securityProperties) {
        this.securityProperties = securityProperties;
    }
}

配置:SmsCodeAuthenticationSecurityConfig

package com.imooc.security.core.authentication.mobile;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;

@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain,HttpSecurity> {

    @Autowired
    private AuthenticationSuccessHandler imoocAuthenticationSuccessHandler;
    @Autowired
    private AuthenticationFailureHandler imoocAuthenticationFailureHandler;
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public void configure(HttpSecurity http) {
        SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
        smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(imoocAuthenticationSuccessHandler);
        smsCodeAuthenticationFilter.setAuthenticationFailureHandler(imoocAuthenticationFailureHandler);

        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);

        http.authenticationProvider(smsCodeAuthenticationProvider)
            .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

使用SpringSecurity开发基于表单的认证

代码重构

使用SpringSecurity开发基于表单的认证

短信验证码与图形验证码的过滤器有很多重复部分,做一个重构。

完整代码

表单认证完整代码