A place to hold mainly reading notes, and some technical stuff occasionally. 这里主要是一些读书笔记、感悟;还有部分技术相关的内容。
目录[-]
本系列教程,是作为团队内部的培训资料准备的。主要以实验的方式来体验SpringSecurity
的各项Feature。
新建一个SpringBoot
项目,起名springboot-security-form
,核心依赖为Web
,SpringSecurity
与Thymeleaf
。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// There is no PasswordEncoder mapped for the id "null"
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
String yourPassword = "123";
System.out.println("Encoded password: " + encoder.encode(yourPassword));
// Config account info and permissions
auth.inMemoryAuthentication()
.withUser("dev").password(encoder.encode(yourPassword)).authorities("p1")
.and()
.withUser("test").password(encoder.encode(yourPassword)).authorities("p2");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user/add").hasAuthority("p1")
.antMatchers("/user/query").hasAuthority("p2")
.antMatchers("/user/**").authenticated()
.anyRequest().permitAll() // Let other request pass
.and()
.httpBasic();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user/add").hasAuthority("p1")
.antMatchers("/user/query").hasAuthority("p2")
.antMatchers("/user/**").authenticated()
.anyRequest().permitAll() // Let other request pass
.and()
.csrf().disable() // turn off csrf, or will be 403 forbidden
.formLogin() // Support form and HTTPBasic
.loginPage("/login");
}
@Controller
public class LoginController {
@GetMapping("/login")
public String login() {
return "login";
}
@GetMapping(value = "/user/add")
@ResponseBody
public String accessResource1() {
return " Access Resource 1: Add User";
}
@GetMapping(value = "/user/query")
@ResponseBody
public String accessResource2() {
return " Access Resource 2: Query User";
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
<form action="login" method="post">
<span>用户名</span><input type="text" name="username" /> <br>
<span>密码</span><input type="password" name="password" /> <br>
<input type="submit" value="登录">
</form>
</body>
</html>
Note:此时,需要先关闭CSRF,.csrf().disable()
,否则报403;
默认登录页面接口与登录数据提交接口是同一个:/login
,顺着.loginPage
,进入FormLoginConfigurer
,源码如下:
@Override
public FormLoginConfigurer<H> loginPage(String loginPage) {
return super.loginPage(loginPage);
}
继续进入父类的loginPage
方法,
protected T loginPage(String loginPage) {
setLoginPage(loginPage);
updateAuthenticationDefaults();
this.customLoginPage = true;
return getSelf();
}
继续跟踪进入方法updateAuthenticationDefaults();
,可以看到,如果没有配置loginProcessingUrl
,那么loginProcessingUrl
与loginPage
便相同。
protected final void updateAuthenticationDefaults() {
if (loginProcessingUrl == null) {
loginProcessingUrl(loginPage);
}
if (failureHandler == null) {
failureUrl(loginPage + "?error");
}
final LogoutConfigurer<B> logoutConfigurer = getBuilder().getConfigurer(
LogoutConfigurer.class);
if (logoutConfigurer != null && !logoutConfigurer.isCustomLogoutSuccess()) {
logoutConfigurer.logoutSuccessUrl(loginPage + "?logout");
}
}
下面我们自定义登录数据提交接口为/formLogin
,此时相应的前端action也要修改。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user/add").hasAuthority("p1")
.antMatchers("/user/query").hasAuthority("p2")
.antMatchers("/user/**").authenticated()
.anyRequest().permitAll() // Let other request pass
.and()
.csrf().disable() // turn off csrf, or will be 403 forbidden
.formLogin() // Support form and HTTPBasic
.loginPage("/login")
.loginProcessingUrl("/formLogin");
}
<form action="formLogin" method="post">
<span>用户名</span><input type="text" name="username" /> <br>
<span>密码</span><input type="password" name="password" /> <br>
<input type="submit" value="登录">
</form>
form
表单中设置用户名、密码分别为username
, password
,那为什么这样写呢,可以改成别的嘛?可以倒是可以,但是不能随便改;username
改为name
,再次尝试登录,后端接口将报错:org.springframework.security.authentication.BadCredentialsException: Bad credentials
。。可是实际项目中我们的用户名密码就是不叫这个名字呢?我们可以进行配置.usernameParameter("name")
:@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user/add").hasAuthority("p1")
.antMatchers("/user/query").hasAuthority("p2")
.antMatchers("/user/**").authenticated()
.anyRequest().permitAll() // Let other request pass
.and()
.csrf().disable() // turn off csrf, or will be 403 forbidden
.formLogin() // Support form and HTTPBasic
.loginPage("/login")
.loginProcessingUrl("/formLogin")
.usernameParameter("name");
}
<form action="formLogin" method="post">
<span>用户名</span><input type="text" name="name" /> <br>
<span>密码</span><input type="password" name="password" /> <br>
<input type="submit" value="登录">
</form>
默认的用户名、密码分别为username
, password
,我们看下SpringSecurity的源码:
public final class FormLoginConfigurer<H extends HttpSecurityBuilder<H>> extends
AbstractAuthenticationFilterConfigurer<H, FormLoginConfigurer<H>, UsernamePasswordAuthenticationFilter> {
/**
* Creates a new instance
* @see HttpSecurity#formLogin()
*/
public FormLoginConfigurer() {
super(new UsernamePasswordAuthenticationFilter(), null);
usernameParameter("username");
passwordParameter("password");
}
}
问题:就以上个实验3中的报错信息为例,或当用户名、密码输错后,如何在后台看到错误信息?
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user/add").hasAuthority("p1")
.antMatchers("/user/query").hasAuthority("p2")
.antMatchers("/user/**").authenticated()
.anyRequest().permitAll() // Let other request pass
.and()
.csrf().disable() // turn off csrf, or will be 403 forbidden
.formLogin() // Support form and HTTPBasic
.loginPage("/login")
.loginProcessingUrl("/formLogin")
.usernameParameter("name")
.failureHandler(new AuthenticationFailureHandler(){
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
exception.printStackTrace();
request.getRequestDispatcher(request.getRequestURL().toString()).forward(request, response);
}
});
}
常见的认证异常,这里可以看到AuthenticationException
共有18个子类:
上述增加了在认证失败时的处理:输出错误信息。同理,如果想在登录成功时直接进行一些处理(eg: 数据初始化等),可以使用以下配置:
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
System.out.println("Login Successfully~");
// do something here: initial work or forward to different url regarding different roles
...
request.getRequestDispatcher("").forward(request, response);
}
})
经历千难万险,终于要登录成功了。进来之后要跳转到哪里呢?看你喽~想跳哪里跳哪里。。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user/add").hasAuthority("p1")
.antMatchers("/user/query").hasAuthority("p2")
.antMatchers("/user/**").authenticated()
.anyRequest().permitAll() // Let other request pass
.and()
.csrf().disable() // turn off csrf, or will be 403 forbidden
.formLogin() // Support form and HTTPBasic
.loginPage("/login")
.loginProcessingUrl("/formLogin")
.usernameParameter("name")
.failureHandler(new AuthenticationFailureHandler(){
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
exception.printStackTrace();
request.getRequestDispatcher(request.getRequestURL().toString()).forward(request, response);
}
})
.successForwardUrl("/ok"); // custom login success page, a POST request
}
@Controller
public class LoginController {
...
@PostMapping(value = "/ok")
@ResponseBody
public String ok() {
return "ok";
}
}
通过.successForwardUrl("/ok")
配置了登录成功之后要跳转的页面路径或接口,同时需要在后端新增/ok
接口。
Note:
successForwardUrl
的接口必须为POST
接口;.successForwardUrl("/ok");
,还可以使用.defaultSuccessUrl("/ok");
或者.defaultSuccessUrl("/ok", true);
第二个参数true
表示不管是从哪个地址进来,登录后全部跳转到指定的地址,此时与successForwardUrl
效果相同,默认为false
,failureForwardUrl
。默认的退出接口是/logout
,可进行配置:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user/add").hasAuthority("p1")
.antMatchers("/user/query").hasAuthority("p2")
.antMatchers("/user/**").authenticated()
.anyRequest().permitAll() // Let other request pass
.and()
.csrf().disable() // turn off csrf, or will be 403 forbidden
.formLogin() // Support form and HTTPBasic
.loginPage("/login")
.loginProcessingUrl("/formLogin")
.usernameParameter("name")
.failureHandler(new AuthenticationFailureHandler(){
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
exception.printStackTrace();
request.getRequestDispatcher(request.getRequestURL().toString()).forward(request, response);
}
})
.successForwardUrl("/ok") // custom login success page, a POST request
.and()
.logout()
.logoutUrl("/leave");
}
上述配置将退出接口改为/leave
。在默认的退出过程中,还做了诸如清除认证信息和使Session失效等工作:
public class SecurityContextLogoutHandler implements LogoutHandler {
protected final Log logger = LogFactory.getLog(this.getClass());
private boolean invalidateHttpSession = true;
private boolean clearAuthentication = true;
// ~ Methods
// ====================================================
/**
* Requires the request to be passed in.
*
* @param request from which to obtain a HTTP session (cannot be null)
* @param response not used (can be <code>null</code>)
* @param authentication not used (can be <code>null</code>)
*/
public void logout(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) {
Assert.notNull(request, "HttpServletRequest required");
if (invalidateHttpSession) {
HttpSession session = request.getSession(false);
if (session != null) {
logger.debug("Invalidating session: " + session.getId());
session.invalidate();
}
}
if (clearAuthentication) {
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(null);
}
SecurityContextHolder.clearContext();
}
}
If you have any questions or any bugs are found, please feel free to contact me.
Your comments and suggestions are welcome!