Spring Security 笔记(2)——RESTful

登陆成功返回 json

修改 configure(HttpSecurity http) 如下:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .loginPage("/login.html")
            .loginProcessingUrl("/login")
            .usernameParameter("username")
            .passwordParameter("passwd")
            .successHandler((req, resp, authentication) -> {
                resp.setContentType("application/json;charset=utf-8");
                PrintWriter writer = resp.getWriter();
                writer.write(new ObjectMapper().writeValueAsString(authentication.getPrincipal()));
                writer.flush();
                writer.close();
            })
            .permitAll()
            .and()
            .logout()
            .logoutRequestMatcher(new AntPathRequestMatcher("/logout", "POST"))
            .and()
            .csrf().disable();
}

主要添加了 successHandler 部分,指定登陆成功后返回的内容,我指定为 json 格式的登录认证信息。

此时使用 POST 方式请求登录接口 127.0.0.1:8080/login?username=bolitao&passwd=bolitao,返回结果如下:

{
    "password": null,
    "username": "bolitao",
    "authorities": [
        {
            "authority": "ROLE_ADMIN"
        },
        {
            "authority": "ROLE_USER"
        }
    ],
    "accountNonExpired": true,
    "accountNonLocked": true,
    "credentialsNonExpired": true,
    "enabled": true
}

登陆失败返回 json

和上节类似,添加 failureHandler 即可应对失败的返回情况:

.failureHandler((req, resp, exception) -> {
    resp.setContentType("application/json;charset=utf-8");
    PrintWriter writer = resp.getWriter();
    String exceptionMessage = exception.getMessage();
    if (exception instanceof LockedException) {
        exceptionMessage = "账户被锁";
    } else if (exception instanceof CredentialsExpiredException) {
        exceptionMessage = "密码过期";
    } else if (exception instanceof AccountExpiredException) {
        exceptionMessage = "账户过期";
    } else if (exception instanceof DisabledException) {
        exceptionMessage = "账户已被禁用";
    } else if (exception instanceof BadCredentialsException) {
        exceptionMessage = "用户名或密码错误";
    }
    writer.write(new ObjectMapper().writeValueAsString(exceptionMessage));
    writer.flush();
    writer.close();
})

这里未作特殊处理,仅返回了具体错误信息。

Q:为什么用户名或密码错误不返回确定的错误呢?账户输错提示用户名错误、密码输错提示密码错误,这样不是更友好吗?

A:为了防止被穷举撞库。

在 Spring Security 源码中,其实确实是有密码错误的异常的,但默认不会返回,只会统一返回 BadCredentialsException

具体见 Spring Security 源码的 AbstractUserDetailsAuthenticationProvider 类:

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
	Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
			() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
					"Only UsernamePasswordAuthenticationToken is supported"));
	String username = determineUsername(authentication);
	boolean cacheWasUsed = true;
	UserDetails user = this.userCache.getUserFromCache(username);
	if (user == null) {
		cacheWasUsed = false;
		try {
			user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
		}
		catch (UsernameNotFoundException ex) {
			this.logger.debug("Failed to find user '" + username + "'");
			if (!this.hideUserNotFoundExceptions) {
				throw ex;
			}
			throw new BadCredentialsException(this.messages
					.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}
		Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
	}
    // ...

可以看到 Spring Security 通过 hideUserNotFoundExceptions 这个布尔值判断是否返回 UsernameNotFoundException(用户不存在异常)。并且 hideUserNotFoundExceptions 的默认值为 true

未登录提示

Spring Security 默认会跳转到 loginURL 让用户进行登陆,在前后端分离场景下返回未登录 json 消息才是更合适的方式。在 config 添加一小段即可实现:

.exceptionHandling().authenticationEntryPoint(
(req, resp, exception) -> {
    resp.setStatus(HttpStatus.UNAUTHORIZED.value());
    resp.setContentType("application/json;charset=utf-8");
    PrintWriter writer = resp.getWriter();
    writer.write(new ObjectMapper().writeValueAsString("尚未登陆"));
    writer.flush();
    writer.close();
});

这里仅返回了一句话,在实际场景应该用统一返回类进行包装。实际效果:

注销成功提示

和上面的登陆成功返回 json 类似,定义一个 lambda 函数描述返回值:

.logoutSuccessHandler((req, resp, authentication)-> {
    resp.setContentType("application/json;charset=utf-8");
    PrintWriter writer = resp.getWriter();
    writer.write(new ObjectMapper().writeValueAsString("注销成功"));
    writer.flush();
    writer.close();
})
加载评论