- 项目创建成功后,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");
}
}
- 首先我们自定义 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 代码中的用户名/密码才能访问成功。
自定义表单登录页
重写它的 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
只需要配置一个即可,具体配置哪个,则要看你的需求,两个的区别如下:
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 只需要配置一个即可。」
登录失败回调
与登录成功相似,登录失败也是有两个方法:
- 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。clearAuthentication
和invalidateHttpSession
分别表示清除认证信息和使 HttpSession 失效,默认可以不用配置,默认就会清除。