Spring Security---详解登录步骤

x33g5p2x  于2021-11-18 转载在 Spring  
字(17.8k)|赞(0)|评价(0)|浏览(775)

步骤分析

1.新建项目

首先新建一个 Spring Boot 项目,创建时引入 Spring Security 依赖和 web 依赖,如下图:

项目创建成功后,Spring Security 的依赖就添加进来了,在 Spring Boot 中我们加入的是 spring-boot-starter-security ,其实主要是这两个:

项目创建成功后,我们添加一个测试的 HelloController,内容如下:

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

接下来什么事情都不用做,我们直接来启动项目。

在项目启动过程中,我们会看到如下一行日志:

Using generated security password: 30abfb1f-36e1-446a-a79b-f70024f589ab

这就是 Spring Security 为默认用户 user 生成的临时密码,是一个 UUID 字符串。

接下来我们去访问 http://localhost:8080/hello 接口,就可以看到自动重定向到登录页面了:

在登录页面,默认的用户名就是 user,默认的登录密码则是项目启动时控制台打印出来的密码,输入用户名密码之后,就登录成功了,登录成功后,我们就可以访问到 /hello 接口了。

在 Spring Security 中,默认的登录页面和登录接口,都是 /login只不过一个是 get 请求(登录页面),另一个是 post 请求(登录接口)。

大家可以看到,非常方便,一个依赖就保护了所有接口。

默认生成密码的源码探究

和用户相关的自动化配置类在 UserDetailsServiceAutoConfiguration 里边,在该类的 getOrDeducePassword 方法中,我们看到如下一行日志:

private String getOrDeducePassword(User user, PasswordEncoder encoder) {
        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;
    }

毫无疑问,我们在控制台看到的日志就是从这里打印出来的。打印的条件是 isPasswordGenerated 方法返回 true,即密码是默认生成的。

进而我们发现,user.getPassword 出现在 SecurityProperties 中,在 SecurityProperties 中我们看到如下定义:

/** * Default user name. */
private String name = "user";
/** * Password for the default user name. */
private String password = UUID.randomUUID().toString();
private boolean passwordGenerated = true;

可以看到,默认的用户名就是 user,默认的密码则是 UUID,而默认情况下,passwordGenerated 也为 true。

2.用户配置

默认的密码有一个问题就是每次重启项目都会变,这很不方便。

在正式介绍数据库连接之前,先介绍两种非主流的用户名/密码配置方案。

2.1 配置文件

我们可以在 application.properties 中配置默认的用户名密码。

怎么配置呢? SecurityProperties,默认的用户就定义在它里边,是一个静态内部类,我们如果要定义自己的用户名密码,必然是要去覆盖默认配置,我们先来看下 SecurityProperties 的定义:

@ConfigurationProperties(prefix = "spring.security")
public class SecurityProperties {

这就很清晰了,我们只需要以 spring.security.user 为前缀,去定义用户名密码即可:

spring.security.user.name=javaboy
spring.security.user.password=123

这就是我们新定义的用户名密码。

properties 中定义的用户名密码最终是通过 set 方法注入到属性中去的,这里我们顺便来看下 SecurityProperties.User#setPassword 方法:

public void setPassword(String password) {
            if (StringUtils.hasLength(password)) {
            //如果自定义了密码,那么默认的打印密码,就不配置了
                this.passwordGenerated = false;
                this.password = password;
            }
        }

从这里我们可以看到,application.properties 中定义的密码在注入进来之后,还顺便设置了 passwordGenerated 属性为 false,这个属性设置为 false 之后,控制台就不会打印默认的密码了。

此时重启项目,就可以使用自己定义的用户名/密码登录了。

2.2 配置类

除了上面的配置文件这种方式之外,我们也可以在配置类中配置用户名/密码。

在配置类中配置,我们就要指定 PasswordEncoder 了,这是一个非常关键的东西。

考虑到有的小伙伴对于 PasswordEncoder 还不太熟悉,因此,我这里先稍微给大家介绍一下 PasswordEncoder 到底是干嘛用的。要说 PasswordEncoder ,就得先说密码加密。

加密方案

密码加密我们一般会用到散列函数,又称散列算法、哈希函数,这是一种从任何数据中创建数字“指纹”的方法。散列函数把消息或数据压缩成摘要,使得数据量变小,将数据的格式固定下来,然后将数据打乱混合,重新创建一个散列值。散列值通常用一个短的随机字母和数字组成的字符串来代表。好的散列函数在输入域中很少出现散列冲突。在散列表和数据处理中,不抑制冲突来区别数据,会使得数据库记录更难找到。我们常用的散列函数有 MD5 消息摘要算法、安全散列算法(Secure Hash Algorithm)。

但是仅仅使用散列函数还不够,为了增加密码的安全性,一般在密码加密过程中还需要加盐,所谓的盐可以是一个随机数也可以是用户名,加盐之后,即使密码明文相同的用户生成的密码密文也不相同,这可以极大的提高密码的安全性。但是传统的加盐方式需要在数据库中有专门的字段来记录盐值,这个字段可能是用户名字段(因为用户名唯一),也可能是一个专门记录盐值的字段,这样的配置比较繁琐。

Spring Security 提供了多种密码加密方案,官方推荐使用 BCryptPasswordEncoder,BCryptPasswordEncoder 使用 BCrypt 强哈希函数,开发者在使用时可以选择提供 strength 和 SecureRandom 实例。strength 越大,密钥的迭代次数越多,密钥迭代次数为 2^strength。strength 取值在 4~31 之间,默认为 10。

不同于 Shiro 中需要自己处理密码加盐,在 Spring Security 中,BCryptPasswordEncoder 就自带了盐,处理起来非常方便。

而 BCryptPasswordEncoder 就是 PasswordEncoder 接口的实现类。

2.2.1 PasswordEncoder

PasswordEncoder 这个接口中就定义了三个方法:

public interface PasswordEncoder {
	String encode(CharSequence rawPassword);
	boolean matches(CharSequence rawPassword, String encodedPassword);
	default boolean upgradeEncoding(String encodedPassword) {
		return false;
	}
}
  1. encode 方法用来对明文密码进行加密,返回加密之后的密文。
  2. matches方法是一个密码校对方法,在用户登录的时候,将用户传来的明文密码和数据库中保存的密文密码作为参数,传入到这个方法中去,根据返回的Boolean 值判断用户密码是否输入正确。
  3. upgradeEncoding 是否还要进行再次加密,这个一般来说就不用了。

通过下图我们可以看到 PasswordEncoder 的实现类:

2.2.2 配置

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
   @Bean
   PasswordEncoder passwordEncoder() {
      return NoOpPasswordEncoder.getInstance();
   }
   @Override
   protected void configure(AuthenticationManagerBuilder auth) throws Exception {
      auth.inMemoryAuthentication()
              .withUser("dhy")
              .password("123456").roles("admin");
   }
}
  • 首先我们自定义 SecurityConfig 继承自 WebSecurityConfigurerAdapter,重写里边的 configure 方法。
  • 首先我们提供了一个 PasswordEncoder 的实例,因为目前的案例还比较简单,因此我暂时先不给密码进行加密,所以返回 NoOpPasswordEncoder 的实例即可。
  • configure 方法中,我们通过 inMemoryAuthentication 来开启在内存中定义用户,withUser 中是用户名,password 中则是用户密码,roles 中是用户角色
  • 如果需要配置多个用户,用 and 相连。

为什么用 and 相连呢?
在没有 Spring Boot 的时候,我们都是 SSM 中使用 Spring Security,这种时候都是在 XML 文件中配置 Spring Security,既然是 XML 文件,标签就有开始有结束,现在的 and 符号相当于就是 XML 标签的结束符,表示结束当前标签,这个时候上下文会回到 inMemoryAuthentication 方法中,然后开启新用户的配置。

配置完成后,再次启动项目,Java 代码中的配置会覆盖掉 XML 文件中的配置,此时再去访问 /hello 接口,就会发现只有 Java 代码中的用户名/密码才能访问成功。

3.自定义表单登录页

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<form action="/login.html" method="post">
  用户名:<input type="text" name="username" /><br/>
  密码:<input type="password" name="password" /><br/>
  <input type="submit" value="登录" />
</form>
</body>
</html>

form 表单中,注意 action 为 /login.html,其他的都是常规操作,我就不重复介绍了。

3.1服务端定义

然后接下来我们继续完善前面的 SecurityConfig 类,继续重写它的 configure(WebSecurity web)configure(HttpSecurity http) 方法,如下:

@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()
              .loginPage("/login.html")
              .permitAll()
              .and()
              .csrf().disable();
   }
  • web.ignoring() 用来配置忽略掉的 URL 地址,一般对于静态文件,我们可以采用此操作。
  • 如果我们使用 XML 来配置 Spring Security ,里边会有一个重要的标签 < http >,HttpSecurity 提供的配置方法 都对应了该标签
  • authorizeRequests 对应了 < intercept-url >
  • formLogin 对应了 < formlogin >
  • and 方法表示结束当前标签,上下文回到HttpSecurity,开启新一轮的配置。
  • permitAll 表示登录相关的页面/接口不要被拦截。
  • 最后记得关闭 csrf ,关于 csrf 问题我到后面专门和大家说。

当我们定义了登录页面为 /login.html 的时候,Spring Security 会帮我们自动注册一个 /login.html 的接口,这个接口是 POST 请求,用来处理登录逻辑

3.登录接口

登录接口是提交登录数据的地方,就是登录页面里边的 form 表单的 action 属性对应的值。

在 Spring Security 中,如果我们不做任何配置,默认的登录页面和登录接口的地址都是 /login,也就是说,默认会存在如下两个请求:

  • GET http://localhost:8080/login
  • POST http://localhost:8080/login

如果是 GET 请求表示你想访问登录页面,如果是 POST 请求,表示你想提交登录数据。

在上面,我们在 SecurityConfig 中自定定义了登录页面地址,如下:

.and()
.formLogin()
.loginPage("/login.html")
.permitAll()
.and()

当我们配置了 loginPage/login.html 之后,这个配置从字面上理解,就是设置登录页面的地址为 /login.html。

实际上它还有一个隐藏的操作,就是登录接口地址也设置成 /login.html 了。换句话说,新的登录页面和登录接口地址都是 /login.html,现在存在如下两个请求:

  • GET http://localhost:8080/login.html
  • POST http://localhost:8080/login.html

前面的 GET 请求用来获取登录页面,后面的 POST 请求用来提交登录数据。

有的小伙伴会感到奇怪?为什么登录页面和登录接口不能分开配置呢?

其实是可以分开配置的!

SecurityConfig 中,我们可以通过 loginProcessingUrl 方法来指定登录接口地址,如下:

.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/doLogin")
.permitAll()
.and()

这样配置之后,登录页面地址和登录接口地址就分开了,各是各的。

此时我们还需要修改登录页面里边的 action 属性,改为 /doLogin,如下:

<form action="/doLogin" method="post">
<!--省略-->
</form>

此时,启动项目重新进行登录,我们发现依然可以登录成功。

那么为什么默认情况下两个配置地址是一样的呢?

我们知道,form 表单的相关配置在 FormLoginConfigurer 中,该类继承自 AbstractAuthenticationFilterConfigurer ,所以当 FormLoginConfigurer 初始化的时候,AbstractAuthenticationFilterConfigurer 也会初始化,在 AbstractAuthenticationFilterConfigurer 的构造方法中,我们可以看到:

protected AbstractAuthenticationFilterConfigurer() {
        this.defaultSuccessHandler = new SavedRequestAwareAuthenticationSuccessHandler();
        this.successHandler = this.defaultSuccessHandler;
        this.setLoginPage("/login");
    }

这就是配置默认的 loginPage/login

另一方面,FormLoginConfigurer 的初始化方法 init 方法中也调用了父类的 init 方法:

public void init(H http) throws Exception {
	super.init(http);
	initDefaultLoginFilter(http);
}

而在父类的 init 方法中,又调用了 updateAuthenticationDefaults,我们来看下这个方法:

protected final void updateAuthenticationDefaults() {
        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");
        }

    }

从这个方法的逻辑中我们就可以看到,如果用户没有给 loginProcessingUrl 设置值的话,默认就使用 loginPage 作为 loginProcessingUrl

而如果用户配置了 loginPage,在配置完 loginPage 之后,updateAuthenticationDefaults 方法还是会被调用,此时如果没有配置 loginProcessingUrl,则使用新配置的 loginPage 作为 loginProcessingUrl

好了,看到这里,相信小伙伴就明白了为什么一开始的登录接口和登录页面地址一样了。

4.登录参数

我们的登录表单中的参数是 username 和 password,注意,默认情况下,这个不能变:

<form action="/login.html" method="post">
    <input type="text" name="username" id="name">
    <input type="password" name="password" id="pass">
    <button type="submit">
      <span>登录</span>
    </button>
</form>

那么为什么是这样呢?

还是回到 FormLoginConfigurer 类中,在它的构造方法中,我们可以看到有两个配置用户名密码的方法:

public FormLoginConfigurer() {
	super(new UsernamePasswordAuthenticationFilter(), null);
	usernameParameter("username");
	passwordParameter("password");
}

在这里,首先 super 调用了父类的构造方法,传入了 UsernamePasswordAuthenticationFilter 实例,该实例将被赋值给父类的 authFilter 属性。

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

getAuthenticationFilter 实际上是父类的方法,在这个方法中返回了 authFilter 属性,也就是一开始设置的
UsernamePasswordAuthenticationFilter 实例,然后调用该实例的 setUsernameParameter 方法去设置登录用户名的参数:

public void setUsernameParameter(String usernameParameter) {
	this.usernameParameter = usernameParameter;
}

这里的设置有什么用呢?当登录请求从浏览器来到服务端之后,我们要从请求的 HttpServletRequest 中取出来用户的登录用户名和登录密码,怎么取呢?还是在 UsernamePasswordAuthenticationFilter 类中,有如下两个方法:

protected String obtainPassword(HttpServletRequest request) {
	return request.getParameter(passwordParameter);
}
protected String obtainUsername(HttpServletRequest request) {
	return request.getParameter(usernameParameter);
}

可以看到,这个时候,就用到默认配置的 username 和 password 了。

当然,这两个参数我们也可以自己配置,自己配置方式如下:

.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/doLogin")
.usernameParameter("name")
.passwordParameter("passwd")
.permitAll()
.and()

配置完成后,也要修改一下前端页面:

<form action="/doLogin" method="post">
    <div class="input">
        <label for="name">用户名</label>
        <input type="text" name="name" id="name">
        <span class="spin"></span>
    </div>
    <div class="input">
        <label for="pass">密码</label>
        <input type="password" name="passwd" id="pass">
        <span class="spin"></span>
    </div>
    <div class="button login">
        <button type="submit">
            <span>登录</span>
            <i class="fa fa-check"></i>
        </button>
    </div>
</form>

注意修改 input 的 name 属性值和服务端的对应。

配置完成后,重启进行登录测试。

4.登录回调

在登录成功之后,我们就要分情况处理了,大体上来说,无非就是分为两种情况:

  • 前后端分离登录
  • 前后端不分登录

两种情况的处理方式不一样。本文我们先来卡第二种前后端不分的登录。

4.1 登录成功回调

在 Spring Security 中,和登录成功重定向 URL 相关的方法有两个:

  • defaultSuccessUrl
  • successForwardUrl

首先我们在配置的时候,defaultSuccessUrlsuccessForwardUrl 只需要配置一个即可,具体配置哪个,则要看你的需求,两个的区别如下:

  • 1.defaultSuccessUrl 有一个重载的方法,我们先说一个参数的 defaultSuccessUrl 方法。如果我们在 defaultSuccessUrl 中指定登录成功的跳转页面为 /index此时分两种情况,如果你是直接在浏览器中输入的登录地址,登录成功后,就直接跳转到 /index,如果你是在浏览器中输入了其他地址,例如 http://localhost:8080/hello,结果因为没有登录,又重定向到登录页面,此时登录成功后,就不会来到 /index ,而是来到 /hello 页面,相当于记住了上一次的请求地址。
  • defaultSuccessUrl 还有一个重载的方法,第二个参数如果不设置默认为 false,也就是我们上面的的情况,如果手动设置第二个参数为 true,则 defaultSuccessUrl 的效果和 successForwardUrl 一致。
  • successForwardUrl 表示不管你是从哪里来的,登录后一律跳转到 successForwardUrl 指定的地址。例如 successForwardUrl 指定的地址为 /index ,你在浏览器地址栏输入 http://localhost:8080/hello,结果因为没有登录,重定向到登录页面,当你登录成功之后,就会服务端跳转到 /index 页面;或者你直接就在浏览器输入了登录页面地址,登录成功后也是来到 /index

相关配置如下:

.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/doLogin")
.usernameParameter("name")
.passwordParameter("passwd")
.defaultSuccessUrl("/index")
.successForwardUrl("/index")
.permitAll()
.and()

注意:实际操作中,defaultSuccessUrl 和 successForwardUrl 只需要配置一个即可。

4.2登录失败回调

与登录成功相似,登录失败也是有两个方法:

  • failureForwardUrl
  • failureUrl

这两个方法在设置的时候也是设置一个即可failureForwardUrl 是登录失败之后会发生服务端跳转,即转发,failureUrl 则在登录失败之后,会发生重定向。

一个转发一个重定向的区别

5.注销登录

注销登录的默认接口是 /logout,我们也可以配置。

.and()
.logout()
.logoutUrl("/logout")
.logoutRequestMatcher(new AntPathRequestMatcher("/logout","POST"))
.logoutSuccessUrl("/index")
.deleteCookies()
.clearAuthentication(true)
.invalidateHttpSession(true)
.permitAll()
.and()

注销登录的配置:

  • 默认注销的 URL/logout,是一个 GET 请求,我们可以通过 logoutUrl 方法来修改默认的注销 URL
  • logoutRequestMatcher 方法不仅可以修改注销 URL,还可以修改请求方式,实际项目中,这个方法和 logoutUrl 任意设置一个即可。
  • logoutSuccessUrl 表示注销成功后要跳转的页面。
  • deleteCookies 用来清除 cookie
  • clearAuthenticationinvalidateHttpSession 分别表示清除认证信息和使 HttpSession 失效,默认可以不用配置,默认就会清除。

6.前后端分离中,使用 JSON 格式登录

1.服务端接口调整

首先大家知道,用户登录的用户名/密码是在 UsernamePasswordAuthenticationFilter 类中处理的,具体的处理代码如下

public Authentication attemptAuthentication(HttpServletRequest request,
		HttpServletResponse response) throws AuthenticationException {
	String username = obtainUsername(request);
	String password = obtainPassword(request);
    //省略
}
protected String obtainPassword(HttpServletRequest request) {
	return request.getParameter(passwordParameter);
}
protected String obtainUsername(HttpServletRequest request) {
	return request.getParameter(usernameParameter);
}

从这段代码中,我们就可以看出来为什么 Spring Security 默认是通过 key/value 的形式来传递登录参数,因为它处理的方式就是 request.getParameter

所以我们要定义成 JSON 的,思路很简单,就是自定义一个过滤器代替 UsernamePasswordAuthenticationFilter ,然后在获取参数的时候,换一种方式就行了。

2.自定义过滤器

接下来我们来自定义一个过滤器代替 UsernamePasswordAuthenticationFilter ,如下:

public class LoginFilter 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());
        }
        String verify_code = (String) request.getSession().getAttribute("verify_code");
        if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
            Map<String, String> loginData = new HashMap<>();
            try {
                loginData = new ObjectMapper().readValue(request.getInputStream(), Map.class);
            } catch (IOException e) {
            }finally {
                String code = loginData.get("code");
                checkCode(response, code, verify_code);
            }
            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);
            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("验证码不正确");
        }
    }
}

这段逻辑我们基本上是模仿官方提供的 UsernamePasswordAuthenticationFilter 来写的,我来给大家稍微解释下:

  • 首先登录请求肯定是 POST,如果不是 POST ,直接抛出异常,后面的也不处理了。
  • 因为要在这里处理验证码,所以第二步从 session 中把已经下发过的验证码的值拿出来。
  • 接下来通过 contentType 来判断当前请求是否通过 JSON 来传递参数,如果是通过 JSON 传递参数,则按照 JSON 的方式解析,如果不是,则调用 super.attemptAuthentication 方法,进入父类的处理逻辑中,也就是说,我们自定义的这个类,既支持 JSON 形式传递参数,也支持 key/value 形式传递参数。
  • 如果是 JSON 形式的数据,我们就通过读取 request 中的 I/O 流,将 JSON 映射到一个 Map 上。
  • 从 Map 中取出 code,先去判断验证码是否正确,如果验证码有错,则直接抛出异常。
  • 接下来从 Map 中取出 username 和 password,构造 UsernamePasswordAuthenticationToken 对象并作校验。

过滤器定义完成后,接下来用我们自定义的过滤器代替默认的 UsernamePasswordAuthenticationFilter,首先我们需要提供一个 LoginFilter 的实例:

@Bean
LoginFilter loginFilter() throws Exception {
    LoginFilter loginFilter = new LoginFilter();
    loginFilter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            response.setContentType("application/json;charset=utf-8");
            PrintWriter out = response.getWriter();
            Hr hr = (Hr) authentication.getPrincipal();
            hr.setPassword(null);
            RespBean ok = RespBean.ok("登录成功!", hr);
            String s = new ObjectMapper().writeValueAsString(ok);
            out.write(s);
            out.flush();
            out.close();
        }
    });
    loginFilter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
            response.setContentType("application/json;charset=utf-8");
            PrintWriter out = response.getWriter();
            RespBean respBean = RespBean.error(exception.getMessage());
            if (exception instanceof LockedException) {
                respBean.setMsg("账户被锁定,请联系管理员!");
            } else if (exception instanceof CredentialsExpiredException) {
                respBean.setMsg("密码过期,请联系管理员!");
            } else if (exception instanceof AccountExpiredException) {
                respBean.setMsg("账户过期,请联系管理员!");
            } else if (exception instanceof DisabledException) {
                respBean.setMsg("账户被禁用,请联系管理员!");
            } else if (exception instanceof BadCredentialsException) {
                respBean.setMsg("用户名或者密码输入错误,请重新输入!");
            }
            out.write(new ObjectMapper().writeValueAsString(respBean));
            out.flush();
            out.close();
        }
    });
    loginFilter.setAuthenticationManager(authenticationManagerBean());
    loginFilter.setFilterProcessesUrl("/doLogin");
    return loginFilter;
}

当我们代替了 UsernamePasswordAuthenticationFilter 之后,原本在 SecurityConfig#configure 方法中关于 form 表单的配置就会失效,那些失效的属性,都可以在配置 LoginFilter 实例的时候配置。

另外记得配置一个 AuthenticationManager,根据 WebSecurityConfigurerAdapter 中提供的配置即可。

FilterProcessUrl 则可以根据实际情况配置,如果不配置,默认的就是 /login

最后,我们用自定义的 LoginFilter 实例代替 UsernamePasswordAuthenticationFilter,如下:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        ...
        //省略
    http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}

调用 addFilterAt 方法完成替换操作。

配置完成后,重启后端,先用 POSTMAN 测试登录接口,如下:

相关文章