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

    
<groupId>
org.springframework.boot
</groupId>

    
<artifactId>
spring-boot-starter-security
</artifactId>

</dependency>

<dependency>

    
<groupId>
org.springframework.boot
</groupId>

    
<artifactId>
spring-boot-starter-web
</artifactId>

</dependency>

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

@RestController
publicclass 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 请求(登录接口)。

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

有人说,你怎么知道知道生成的默认密码是一个 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。

配置文件

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

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

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

自定义表单登录页

重写它的 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 请求,用来处理登录逻辑。

定制 Spring Security 中的表单登录

  • 登录接口则是提交登录数据的地方,就是登录页面里边的 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() {
	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

登录参数

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()

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

登录回调

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

defaultSuccessUrl 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 只需要配置一个即可。」

登录失败回调

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

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

注销登录

注销登录的默认接口是 /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 失效,默认可以不用配置,默认就会清除。