Spring Security自定义用户认证
目录
Spring Security自定义用户认证
自定义用户认证过程
替换默认登录页
处理成功和失败
自定义登录成功逻辑
自定义登录失败逻辑
自定义用户认证过程
上一节中我们简单搭建了个Spring Boot + Spring Security的项目,认证的用户名和密码都是由Spring Security生成。Spring Security支持我们自定义认证的过程,如处理用户信息获取逻辑,使用我们自定义的登录页面替换Spring Security默认的登录页及自定义登录成功或失败后的处理逻辑等,这里将在上一节的源码基础上进行改造。自定义认证的过程需要实现Spring Security提供的UserDetailService
接口,该接口只有一个抽象方法loadUserByUsername
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
loadUserByUsername
方法返回一个UserDetail
对象,该对象也是一个接口,包含一些用于描述用户信息的方法
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
getAuthorities
获取用户包含的权限,返回权限集合,权限是一个继承了GrantedAuthority
的对象;getPassword
和getUsername
用于获取密码和用户名;isAccountNonExpired
方法返回boolean类型,用于判断账户是否未过期,未过期返回true反之返回false;isAccountNonLocked
方法用于判断账户是否未锁定;isCredentialsNonExpired
用于判断用户凭证是否没过期,即密码是否未过期;isEnabled
方法用于判断用户是否可用。
实际中我们可以自定义UserDetails
接口的实现类,也可以直接使用Spring Security提供的UserDetails
接口实现类org.springframework.security.core.userdetails.User
。首先创建一个MyUser
对象,用于存放模拟的用户数据
public class MyUser implements Serializable {
private static final long serialVersionUID = 3497935890426858541L;
private String userName;
private String password;
private boolean accountNonExpired = true;
private boolean accountNonLocked= true;
private boolean credentialsNonExpired= true;
private boolean enabled= true;
}
接着创建MyUserDetailService
实现UserDetailService
@Configuration
public class UserDetailService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 模拟一个用户,替代数据库获取逻辑
MyUser user = new MyUser();
user.setUserName(username);
user.setPassword(this.passwordEncoder.encode("123456"));
// 输出加密后的密码
System.out.println(user.getPassword());
return new User(username, user.getPassword(), user.isEnabled(),
user.isAccountNonExpired(), user.isCredentialsNonExpired(),
user.isAccountNonLocked(), AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
包含一个三个参数的构造器User(String username, String password,Collection<? extends GrantedAuthority> authorities)
,由于权限参数不能为空,所以这里先使用AuthorityUtils.commaSeparatedStringToAuthorityList
方法模拟一个admin的权限,该方法可以将逗号分隔的字符串转换为权限集合。
此外我们还注入了PasswordEncoder
对象,该对象用于密码加密,注入前需要手动配置。我们在BrowserSecurityConfig
中配置它
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
PasswordEncoder
是一个密码加密接口,而BCryptPasswordEncoder
是Spring Security提供的一个实现方法,我们也可以自己实现PasswordEncoder
。不过Spring Security实现的BCryptPasswordEncoder
已经足够强大,它对相同的密码进行加密后可以生成不同的结果。
替换默认登录页
直接在src/main/resources/resources目录下定义一个login.html,要怎么做才能让Spring Security跳转到我们自己定义的登录页面呢?很简单,只需要在BrowserSecurityConfig
的configure
中添加一些配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 表单登录
// http.httpBasic() // HTTP Basic
.loginPage("/authentication/require") // 登录跳转 URL
.loginProcessingUrl("/login") // 处理表单登录 URL
.and()
.authorizeRequests() // 授权配置
.antMatchers("/authentication/require", "/login.html").permitAll() // 登录跳转 URL 无需认证
.anyRequest() // 所有请求
.authenticated() // 都需要认证
.and().csrf().disable();
}
然后定义一个控制器BrowserSecurityController
,处理这个请求
@RestController
public class BrowserSecurityController {
private RequestCache requestCache = new HttpSessionRequestCache();
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@GetMapping("/authentication/require")
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public String requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
SavedRequest savedRequest = requestCache.getRequest(request, response);
if (savedRequest != null) {
String targetUrl = savedRequest.getRedirectUrl();
if (StringUtils.endsWithIgnoreCase(targetUrl, ".html"))
redirectStrategy.sendRedirect(request, response, "/login.html");
}
return "访问的资源需要身份认证!";
}
}
根据请求是否以.html
为结尾来对应不同的处理方法。如果是以.html
结尾,那么重定向到登录页面,否则返回”访问的资源需要身份认证!”信息,并且HTTP状态码为401(HttpStatus.UNAUTHORIZED
)。
这样当我们访问http://localhost:8080/hello的时候页面便会跳转到http://localhost:8080/authentication/require,并且输出”访问的资源需要身份认证!”,当我们访问http://localhost:8080/hello.html的时候,页面将会跳转到登录页面。
处理成功和失败
自定义登录成功逻辑
要改变默认的处理成功逻辑很简单,只需要实现org.springframework.security.web.authentication.AuthenticationSuccessHandler
接口的onAuthenticationSuccess
方法即可:
@Component
public class MyAuthenticationSucessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(mapper.writeValueAsString(authentication));
}
}
其中Authentication
参数既包含了认证请求的一些信息,比如IP,请求的SessionId等,也包含了用户信息,即前面提到的User
对象。通过上面这个配置,用户登录成功后页面将打印出Authentication
对象的信息。
要使这个配置生效,我们还的在BrowserSecurityConfig
的configure
中配置它:
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyAuthenticationSucessHandler authenticationSucessHandler;
}
我们将MyAuthenticationSucessHandler
注入进来,并通过successHandler
方法进行配置。这时候重启项目登录后页面将会输出如下JSON信息,像password
,credentials
这些敏感信息,Spring Security已经将其屏蔽。
通过上面配置,登录成功后页面将跳转回引发跳转的页面。如果想指定跳转的页面,比如跳转到/index
,可以将savedRequest.getRedirectUrl()
修改为/index
:
@Component
public class MyAuthenticationSucessHandler implements AuthenticationSuccessHandler {
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
redirectStrategy.sendRedirect(request, response, "/index");
}
}
然后在TestController中定义一个处理该请求的方法:
@RestController
public class TestController {
@GetMapping("index")
public Object index(Authentication authentication) {
return authentication;
}
}
自定义登录失败逻辑
和自定义登录成功处理逻辑类似,自定义登录失败处理逻辑需要实现org.springframework.security.web.authentication.AuthenticationFailureHandler
的onAuthenticationFailure
方法:
不同的失败原因对应不同的异常,比如用户名或密码错误对应的是BadCredentialsException
,用户不存在对应的是UsernameNotFoundException
,用户被锁定对应的是LockedException
等。
假如我们需要在登录失败的时候返回失败信息,可以这样处理:
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Autowired
private ObjectMapper mapper;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(mapper.writeValueAsString(exception.getMessage()));
}
}
同样的,我们需要在BrowserSecurityConfig
的configure
中配置它:
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyAuthenticationSucessHandler authenticationSucessHandler;
@Autowired
private MyAuthenticationFailureHandler authenticationFailureHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 表单登录
// http.httpBasic() // HTTP Basic
.loginPage("/authentication/require") // 登录跳转 URL
.loginProcessingUrl("/login") // 处理表单登录 URL
.successHandler(authenticationSucessHandler) // 处理登录成功
.failureHandler(authenticationFailureHandler) // 处理登录失败
.and()
.authorizeRequests() // 授权配置
.antMatchers("/authentication/require", "/login.html").permitAll() // 登录跳转 URL 无需认证
.anyRequest() // 所有请求
.authenticated() // 都需要认证
.and().csrf().disable();
}
}
当输入错误的密码时,页面输出如下