spring security 是 spring 家族的一个安全框架,入门简单。对比 shiro,它自带登录页面,自动完成登录操作。权限过滤时支持 http 方法过滤。
在新手入门使用时,只需要简单的配置,即可实现登录以及权限的管理,无需自己写功能逻辑代码。
但是对于现在大部分前后端分离的 web 程序,尤其是前端普遍使用 ajax 请求时,spring security 自带的登录系统就有一些不满足需求了。
因为 spring security 有自己默认的登录页,自己默认的登录控制器。而登录成功或失败,都会返回一个 302 跳转。登录成功跳转到主页,失败跳转到登录页。如果未认证直接访问也会跳转到登录页。但是如果前端使用 ajax 请求,ajax 是无法处理 302 请求的。前后端分离 web 中,规范是使用 json 交互。我们希望登录成功或者失败都会返回一个 json。况且 spring security 自带的登录页太丑了,我们还是需要使用自己的。
spring security 一般简单使用:
web 的安全控制一般分为两个部分,一个是认证,一个是授权。
认证管理:
就是认证是否为合法用户,简单的说是登录。一般为匹对用户名和密码,即认证成功。
在 spring security 认证中,我们需要注意的是:哪个类表示用户?哪个属性表示用户名?哪个属性表示密码?怎么通过用户名取到对应的用户?密码的验证方式是什么?
只要告诉 spring security 这几个东西,基本上就可以了。
[
](javascript:void(0); “复制代码”)
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
[
](javascript:void(0); “复制代码”)
事实上只要继承 WebSecurityConfigurerAdapter ,spring security 就已经启用了,当你访问资源时,它就会跳转到它自己默认的登录页。但是这还不行,
当用户点击登录时,
-
它会拿到用户输入的用户名密码;
-
根据用户名通过 UserDetailsService 的 loadUserByUsername(username) 方法获得一个用户对象;
-
获得一个 UserDetails 对象,获得内部的成员属性 password;
-
通过 PasswordEncoder 的 matchs(s1, s2) 方法对比用户的输入的密码和第 3 步的密码;
-
匹配成功;
所以我们要实现这三个接口的三个方法:
- 实现 UserDetailsService ,可以选择同时实现用户的正常业务方法和 UserDetailsService ;
例如:UserServiceImpl implement IUserService,UserDetailsService {}
- 实现 UserDetails ,一般使用用户的实体类实现此接口。
其中有 getUsername(), getPassword(), getAuthorities() 为获取用户名,密码,权限。可根据个人情况实现。
- 实现 PasswordEncoder ,spring security 提供了多个该接口的实现类,可百度和查看源码理解,也可以自己写。
三个实现类的配置如下:
[
](javascript:void(0); “复制代码”)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(NoOpPasswordEncoder.getInstance());
}
}
[
](javascript:void(0); “复制代码”)
其中 Userdetails 为 UserDetailsService 中 loadUserByUsername() 方法的返回值类型。
到目前为止,就可以完成简单认证了。而授权管理,到现在,是默认的:所有资源都只有‘认证’权限,所有用户也只有‘认证’权限。即,经过认证就可以访问所有资源。
以上,就是 spring security 的简易应用。可以实现一个稍微完整的安全控制。非常简单。
授权管理:
授权管理,是在已认证的前提下。用户在认证后,根据用户的不同权限,开放不同的资源。
根据 RBAC 设计,用户有多个角色,角色有多个权限。(真正控制资源的是权限,角色只是一个权限列表,方便使用。)
每个用户都有一个权限列表,授权管理,就是权限和资源的映射。在编程中,写好对应关系。然后当用户请求资源时,查询用户是否有资源对应的权限决定是否通过。
权限写在数据库,配置文件或其他任何地方。只要调用 loadUserByUsername() 时返回的 UserDetails 对象中的 getAuthorities() 方法能获取到。
所以无论用户的权限写在哪里,只要 getAuthorities() 能得到就可以了。
举例:
授权管理映射:add/api/add,query/api/query;
数据库中存储了用户权限:query;
那么该用户就只能访问 / api/query,而不能访问 / api/add。
授权管理配置如下:
[
](javascript:void(0); “复制代码”)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(NoOpPasswordEncoder.getInstance());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(HttpMethod.POST, "/api/data").hasAuthority("add")
.antMatchers(HttpMethod.GET, "/api/data").hasAuthority("query")
.antMatchers("/home").hasAuthority("base");
}
}
[
](javascript:void(0); “复制代码”)
以上就是 spring security 的基本应用。下面是解决前后端分离下的无法 302 跳转的情况。
需求是:前后端分离,需要自己的登录页面,使用 ajax 请求。
出现问题:自己的登录页面请求登录后,后端返回 302 跳转主页,ajax 无法处理;未认证请求资源时,后端返回 302 跳转登录页,也无法处理。
解决思想:修改 302 状态码,修改为 401,403 或者 200 和 json 数据。
HttpSecurity 有很多方法,可以看一看
比如 设置登录页(formLogin().loginPage(“/login.html”)) 可以设置自己的登录页(该设置主要是针对使用 302 跳转,且有自己的登录页,如果不使用 302 跳转,前后端完全分离,无需设置)。
比如 设置认证成功处理
比如 设置认证失败处理
比如 设置异常处理
比如 设置退出成功处理
可以继承重写其中的主要方法(里面有 httpResponse 对象,可以随便返回任何东西)
例如:
[
](javascript:void(0); “复制代码”)
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setStatus(HttpStatus.OK.value());
}
}
[
](javascript:void(0); “复制代码”)
设置完成登录成功和失败处理后,还是不够满足需求,当用户未通过登录页进入网站,我们需要在用户直接访问资源时,告诉前端此用户未认证。(默认是 302 跳转到登录页)。我们可以改成返回 403 状态码。
这里就需要实现一个特殊的方法:AuthenticationEntryPoint 接口的 commence() 方法。
这个方法主要是,用户未认证访问资源时,所做的处理。
spring security 给我们提供了很多现成的 AuthenticationEntryPoint 实现类,
比如默认的 302 跳转登录页,比如返回 403 状态码,还比如返回 json 数据等等。当然也可以自己写。和上面的登录处理一样,实现接口方法,将实现类实例传到配置方法(推荐 spring 注入)。
如下:
[
](javascript:void(0); “复制代码”)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Qualifier("userService")
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private LoginSuccessHandler loginSuccessHandler;
@Autowired
private LoginFailureHandler loginFailureHandler;
@Autowired
private MyLogoutHandler logoutHandler;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(NoOpPasswordEncoder.getInstance());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.loginProcessingUrl("/login")
// 登录成功
.successHandler(loginSuccessHandler)
// 登录失败
.failureHandler(loginFailureHandler).permitAll()
.and()
// 注销成功
.logout().logoutSuccessHandler(logoutHandler)
.and()
// 未登录请求资源
.exceptionHandling().authenticationEntryPoint(new Http403ForbiddenEntryPoint())
.and()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/api/data").hasAuthority("add")
.antMatchers(HttpMethod.GET, "/api/data").hasAuthority("query")
.antMatchers("/home").hasAuthority("base");
}
}
[
](javascript:void(0); “复制代码”)
以上就算是完了,前端发起 ajax 请求时,后端会返回 200,401,403 状态码,前端可根据状态码做相应的处理。
以下是我的全部代码(后端,安全管理 demo)