SpringSecurity系列之自定义表单登录

1 SpringSecurity的初体验
1.1 创建项目

项目创建成功后,Spring Security 的依赖就添加进来了,在 Spring Boot 中我们加入的是 spring-boot-starter-security

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

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

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

接下来我们去访问 http://localhost:8080/hello 接口(接口代码这里就不展示了),就可以看到自动重定向到登录页面了:

在这里插入图片描述

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

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

有人说,你怎么知道知道生成的默认密码是一个 UUID 呢?

这个其实很好判断。

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

if (user.isPasswordGenerated()) {
	logger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword()));
}

毫无疑问,我们在控制台看到的日志就是从这里打印出来的。打印的条件是 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。

1.2 配置用户

和大家介绍两种非主流的用户名/密码配置方案

1.2.1 配置文件

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

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

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

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

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

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

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

public void setPassword(String password) {
	if (!StringUtils.hasLength(password)) {
		return;
	}
	this.passwordGenerated = false;
	this.password = password;
}

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

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

1.2.2 配置类

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

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

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

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

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

配置用户信息:

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

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

1.3自定义表单登录页

然后接下来我们继续完善前面的 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();
}
  1. web.ignoring() 用来配置忽略掉的 URL 地址,一般对于静态文件,我们可以采用此操作。
  2. 如果我们使用 XML 来配置 Spring Security ,里边会有一个重要的标签 <http>HttpSecurity提供的配置方法 都对应了该标签。 authorizeRequests 对应了 <intercept-url>formLogin
    对应了 <formlogin>
  3. and 方法表示结束当前标签,上下文回到HttpSecurity,开启新一轮的配置。
  4. permitAll 表示登录相关的页面/接口不要被拦截。
  5. 最后记得关闭 csrf ,关于 csrf 问题我到后面专门和大家说。
  6. 当我们定义了登录页面为 /login.html 的时候,Spring Security 也会帮我们自动注册一个 /login.html
    的接口,这个接口是 POST 请求,用来处理登录逻辑。
2登陆接口

很多初学者分不清登录接口和登录页面,这个我也很郁闷。我还是在这里稍微说一下,登录页面就是你看到的浏览器展示出来的页面,登录接口则是提交登录数据的地方,就是登录页面里边的 form 表单的 action 属性对应的值。

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

  1. GET http://localhost:8080/login
  2. POST http://localhost:8080/login

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

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

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

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

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

  1. GET http://localhost:8080/login.html
  2. 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() {
	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 (loginProcessingUrl == null) {
		loginProcessingUrl(loginPage);
	}
	//省略
}

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

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

3、登录参数

说完登录接口,我们再来说登录参数。

我们的登录表单中的参数是 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 属性。

接下来 usernameParameter 方法如下:

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

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.登录回调

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

  1. 前后端分离登录
  2. 前后端不分登录

两种情况的处理方式不一样。本文我们先来卡第二种前后端不分的登录,前后端分离的登录回调我在下篇文章中再来和大家细说。

4.1 登录成功回调

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

  1. defaultSuccessUrl
  2. successForwardUrl

这两个咋看没什么区别,实际上内藏乾坤。
首先我们在配置的时候,defaultSuccessUrl 和 successForwardUrl 只需要配置一个即可,具体配置哪个,则要看你的需求,两个的区别如下:

  1. defaultSuccessUrl 有一个重载的方法,我们先说一个参数的 defaultSuccessUrl 方法。如果我们在 defaultSuccessUrl 中指定登录成功的跳转页面为 /index,此时分两种情况,如果你是直接在浏览器中输入的登录地址,登录成功后,就直接跳转到/index,如果你是在浏览器中输入了其他地址,例如http://localhost:8080/hello,结果因为没有登录,又重定向到登录页面,此时登录成功后,就不会来到 /index,而是来到 /hello 页面。
  2. defaultSuccessUrl 还有一个重载的方法,第二个参数如果不设置默认为false,也就是我们上面的的情况,如果手动设置第二个参数为 true,则 defaultSuccessUrl 的效果和successForwardUrl 一致。
  3. 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 只需要配置一个即可。」

使用successForwardUrl 引起的跳转页面报 405 错误,解决办法:

  1. defaultSuccessUrl和successForwardUrl进行路径跳转的底层实现的方式不同,前者是重定向,后者是转发,这点从浏览器地址栏地址可以得到验证。
  2. registry.addViewController("/success").setViewName(“success”);这种路径映射方式不支持post请求,详细解析https://blog.csdn.net/weixin_42074868/article/details/85001093
  3. 重定向和转发的区别:其中之一的区别是,重定向浏览器地址栏会发生变化,为重定向路径(/seccess),请求类型相应的为/seccess的类型为get;转发浏览器地址栏不会发生变化,还是登录处理请求路径(/doLogin),所以相应的请求类型为post,所以就产生了异常。
  4. 如果非要用successForwardUrl进行路径跳转,可以不用addViewController的方式,而是写一个controller,在控制器里进行路径映射。
4.2 登录失败回调

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

  1. failureForwardUrl
  2. failureUrl
.failureForwardUrl("/login.html")
                .failureUrl("/login.html")

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

5.注销登录

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

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

注销登录的配置我来说一下:

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

最后附上详细的配置

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    /**
     * 基于配置类的方式紫丁永刚用户名和密码,不使用默认的
     * 配置用户名和密码,这种是基于内存的方式保存用户名和密码
     * .roles() 这个方法一定要加上,否则会报错
     * @param
     * @throws Exception
     */
   /* @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("jiangheng").password("123456").roles("admin");
    }*/

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/js/**", "/css/**","/images/**");
    }

  
   @Override
    public void configure(WebSecurity web) throws Exception {
        //忽略掉的 URL地址,一般对于静态文件
        web.ignoring().antMatchers("/js/**", "/css/**", "/images/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest()
                //所有的请求必须登录后才能访问
                .authenticated()
                //and() 相当于 xml 文件中的</>,相当于一个配置的结束
                .and()
                .formLogin()
                //登录页面的地址
                .loginPage("/login.html")
                //可以使用这个方法来配置登录接口的地址,如果不设置,那么登录接口就是/login.html
                .loginProcessingUrl("/doLogin")
                //设置form 表单中中参数名称,如果不设置的话,默认是 username 和 password
                .usernameParameter("name")
                .passwordParameter("passwd")
                // 登录成功进行回调
                //defaultSuccessUrl()successForwardUrl() 这个两个方法选择一个就行
                //successForwardUrl()登录成功后一律跳到这个页面
                .successForwardUrl("/index")
                .failureForwardUrl("/login.html")
                .and()
                //登录失败回调
                .logout()
                .logoutUrl("/logout")
                //注销成功后跳转的页面
                .logoutSuccessUrl("/login.html")
                //清除cookie
                .deleteCookies()
                .permitAll()
                .and()
                .csrf()
                .disable();
    }
}

参考文章
https://mp.weixin.qq.com/s/Q0GkUb1Nt6ynV22LFHuQrQ

https://mp.weixin.qq.com/s/kHJRKwH-WUx-JEeaQMa7jw

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值