自定义登录页
替换默认登陆页
将自定义的登录页放到resources目录下,然后在WebSecurityConfigurerAdapter的实现类重写configure方法。
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/js/**", "/css/**", "/images/**"); // 静态资源放行,不进行权限控制
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 表单方式认证
// .httpBasic() // HttpBasic方式认证
.loginPage("/login.html") // 登陆页面
.loginProcessingUrl("/login") // 登陆鉴权地址,这里的login为SpringSecurity默认的鉴权地址
.and()
.authorizeRequests() // 授权配置
.antMatchers("/login.html").permitAll() // 不用鉴权的资源
.anyRequest() // 所有请求
.authenticated() // 都需要认证
.and().csrf().disable(); // 关闭CSRF攻击防御
}
根据访问资源类型进行差异化处理
处理效果:在未登录的情况下,访问html资源的时候跳转到登录页,访问其他类型资源返回JSON格式错误信息,状态码为401。
首先在WebSecurityConfigurerAdapter的实现类重写configure方法。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 表单方式认证
// .httpBasic() // HttpBasic方式认证
.loginPage("/authentication/require") // 登录跳转URL
.loginProcessingUrl("/login") // 登陆鉴权地址,这里的login为SpringSecurity默认的鉴权地址
.and()
.authorizeRequests() // 授权配置
.antMatchers("/authentication/require", "/login.html").permitAll() // 不用鉴权的资源
.anyRequest() // 所有请求
.authenticated() // 都需要认证
.and().csrf().disable() // 关闭CSRF攻击防御
;
}
然后定义一个控制器BrowserSecurityController,处理这个请求:
@RestController
public class BrowserSecurityController {
private RequestCache requestCache = new HttpSessionRequestCache();
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@GetMapping("/authentication/require")
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ResponseBody
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 "{\"errMsg\":\"访问的资源需要身份认证!\"}";
}
}
其中HttpSessionRequestCache为Spring Security提供的用于缓存请求的对象,通过调用它的getRequest方法可以获取到本次请求的HTTP信息。DefaultRedirectStrategy的sendRedirect为Spring Security提供的用于处理重定向的方法。
上面代码获取了引发跳转的请求,根据请求是否以.html为结尾来对应不同的处理方法。如果是以.html结尾,那么重定向到登录页面,否则返回”访问的资源需要身份认证!”信息,并且HTTP状态码为401(HttpStatus.UNAUTHORIZED)。
这样当我们访问http://localhost:8080/hello的时候页面便会跳转到http://localhost:8080/authentication/require,并且输出:{“errMsg”:“访问的资源需要身份认证!”},当我们访问http://localhost:8080/hello.html的时候,页面将会跳转到登录页面。
处理成功和失败
Spring Security有一套默认的处理登录成功和失败的方法:当用户登录成功时,页面会跳转到登录页面并引发登陆操作,比如在未登录的情况下访问http://localhost:8080/hello,页面会跳转到登录页,登录成功后再跳转回来;登录失败时则是跳转到Spring Security默认的错误提示页面。下面我们通过一些自定义配置来替换这套默认的处理机制。
自定义登录成功逻辑
要改变默认的处理成功逻辑很简单,只需要实现org.springframework.security.web.authentication.AuthenticationSuccessHandler接口的onAuthenticationSuccess方法即可:
@Component
public class CustAuthenticationSucessHandler 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(new ObjectMapper().writeValueAsString(authentication));
}
}
其中Authentication参数既包含了认证请求的一些信息,比如IP,请求的SessionId等,也包含了用户信息,即前面提到的User对象。通过上面这个配置,用户登录成功后页面将打印出Authentication对象的信息。
要使这个配置生效,还要在WebSecurityConfigurerAdapter实现类的configure方法中进行配置:
@Autowired
private CustAuthenticationSucessHandler authenticationSucessHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 表单方式认证
// .httpBasic() // HttpBasic方式认证
.loginPage("/authentication/require") // 登录跳转URL
.loginProcessingUrl("/login") // 登陆鉴权地址,这里的login为SpringSecurity默认的鉴权地址
.successHandler(authenticationSucessHandler) // 处理登录成功
.and()
.authorizeRequests() // 授权配置
.antMatchers("/authentication/require", "/login.html").permitAll() // 不用鉴权的资源
.anyRequest() // 所有请求
.authenticated() // 都需要认证
.and().csrf().disable() // 关闭CSRF攻击防御
;
}
此时重启项目登录后页面将会输出Authentication对象的JSON信息。
除此之外,我们也可以在登录成功后做页面的跳转,修改MyAuthenticationSucessHandler:
@Component
public class CustAuthenticationSucessHandler implements AuthenticationSuccessHandler {
private RequestCache requestCache = new HttpSessionRequestCache();
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
SavedRequest savedRequest = requestCache.getRequest(request, response);
redirectStrategy.sendRedirect(request, response, savedRequest.getRedirectUrl());
}
}
通过上面配置,登录成功后页面将跳转回引发跳转的页面。如果想指定跳转的页面,比如跳转到/test/index,可以将savedRequest.getRedirectUrl()修改为/test/index:
@Component
public class CustAuthenticationSucessHandler implements AuthenticationSuccessHandler {
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
redirectStrategy.sendRedirect(request, response, "/test/index");
}
}
然后在TestController中定义一个处理该请求的方法:
@RestController
@RequestMapping("test")
public class TestController {
@GetMapping("index")
public Object index(){
return SecurityContextHolder.getContext().getAuthentication();
}
}
登录成功后,便可以使用SecurityContextHolder.getContext().getAuthentication()获取到Authentication对象信息。除了通过这种方式获取Authentication对象信息外,也可以使用下面这种方式:
@RestController
@RequestMapping("test")
public class TestController {
@GetMapping("index")
public Object index(Authentication authentication) {
return authentication;
}
}
重启项目,登录成功后,页面将跳转到http://localhost:8080/index。
自定义登录失败逻辑
和自定义登录成功处理逻辑类似,自定义登录失败处理逻辑需要实现org.springframework.security.web.authentication.AuthenticationFailureHandler的onAuthenticationFailure方法:
@Component
public class CustAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException {
}
}
onAuthenticationFailure方法的AuthenticationException参数是一个抽象类,Spring Security根据登录失败的原因封装了许多对应的实现类,不同的失败原因对应不同的异常,比如用户名或密码错误对应的是BadCredentialsException,用户不存在对应的是UsernameNotFoundException,用户被锁定对应的是LockedException等。
如果我们需要在登录失败的时候返回失败信息,可以这样处理:
@Component
public class CustAuthenticationFailureHandler implements AuthenticationFailureHandler {
@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(new ObjectMapper().writeValueAsString(exception.getMessage()));
}
}
状态码定义为500(HttpStatus.INTERNAL_SERVER_ERROR.value()),即系统内部异常。
同样,我们需要在WebSecurityConfigurerAdapter实现类的configure方法中进行配置:
@Autowired
private CustAuthenticationFailureHandler authenticationFailureHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 表单方式认证
// .httpBasic() // HttpBasic方式认证
.loginPage("/authentication/require") // 登录跳转URL
.loginProcessingUrl("/login") // 登陆鉴权地址,这里的login为SpringSecurity默认的鉴权地址
.successHandler(authenticationSucessHandler) // 处理登录成功
.failureHandler(authenticationFailureHandler) // 处理登录失败
.and()
.authorizeRequests() // 授权配置
.antMatchers("/authentication/require", "/login.html").permitAll() // 不用鉴权的资源
.anyRequest() // 所有请求
.authenticated() // 都需要认证
.and().csrf().disable() // 关闭CSRF攻击防御
;
}
自定义用户认证
自定义认证的过程需要实现Spring Security提供的UserDetailService接口,该接口只有一个抽象方法loadUserByUsername,源码如下:
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
loadUserByUsername方法返回一个UserDetail对象,该对象也是一个接口,包含一些用于描述用户信息的方法,主要方法的含义如下:
- getAuthorities:获取用户包含的权限,返回权限集合,权限是一个继承了GrantedAuthority的对象;
- getPassword和getUsername方法:用于获取密码和用户名;
- isAccountNonExpired方法:返回boolean类型,用于判断账户是否未过期,未过期返回true反之返回false;
- isAccountNonLocked方法:用于判断账户是否未锁定;
- isCredentialsNonExpired方法:用于判断用户凭证是否没过期,即密码是否未过期;
- isEnabled方法:用于判断用户是否可用。
实现(使用任意用户名及密码admin登陆)
-
创建UserDetailsService的实现类,自定义密码处理逻辑:
@Configuration public class CustUserDetailServiceImpl implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 模拟密码获取逻辑 String password = this.passwordEncoder.encode("admin"); // 输出加密后的密码 System.out.println(password); return new User(username, password, true, true, true, true, AuthorityUtils.commaSeparatedStringToAuthorityList("admin")); } }
-
创建PasswordEncoder对象
CustUserDetailServiceImpl还注入了PasswordEncoder对象,该对象用于密码加密,注入前需要手动配置。添加PasswordEncoder的实例:@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }