Spring Security的HTTP权限控制
简介
一个能够为基于Spring的企业应用系统提供声明式的安全訪问控制解决方式的安全框架(简单说是对访问权限进行控制嘛),应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。 spring security的主要核心功能为 认证和授权,所有的架构也是基于这两个核心功能去实现的。
过滤器与核心组件
springSecurity在我们进行用户认证以及授予权限的时候,通过各种各样的拦截器来控制权限的访问,从而实现安全。
主要过滤器
框架的核心组件
实战
在springboot中整合springSecurity
添加pom.xml依赖
org.springframework.boot
spring-boot-starter-security
编写Java配置文件
自定义了一个springSecurity安全框架的配置类 继承WebSecurityConfigurerAdapter,重写其中的方法configure,在web容器启动的过程中该类实例对象会被WebSecurityConfiguration类处理。
import com.wqy.springbootes.security.AuthFilter;
import com.wqy.springbootes.security.AuthProvider;
import com.wqy.springbootes.security.LoginAuthFailHandler;
import com.wqy.springbootes.security.LoginUrlEntryPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* Created by wqy.
*
* 密码:admin/admin
*/
@EnableWebSecurity
@EnableGlobalMethodSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* HTTP权限控制
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(authFilter(), UsernamePasswordAuthenticationFilter.class);
// 资源访问权限
http.authorizeRequests()
.antMatchers("/admin/login").permitAll() // 管理员登录入口
.antMatchers("/static/**").permitAll() // 静态资源
.antMatchers("/user/login").permitAll() // 用户登录入口
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").hasAnyRole("ADMIN", "USER")
.antMatchers("/api/user/**").hasAnyRole("ADMIN",
"USER")
.and()
.formLogin()
.loginProcessingUrl("/login") // 配置角色登录处理入口
.failureHandler(authFailHandler())
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/logout/page")
.deleteCookies("JSESSIONID")
.invalidateHttpSession(true)
.and()
.exceptionHandling()
.authenticationEntryPoint(urlEntryPoint())// AuthenticationEntryPoint 用来解决匿名用户访问无权限资源时的异常
.accessDeniedPage("/403");
http.csrf().disable(); // 关闭默认打开的crsf protection
http.headers().frameOptions().sameOrigin();// 支持SAMEORIGIN的设置方式
}
/**
* 自定义认证策略
*/
@Autowired
public void configGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider()).eraseCredentials(true);
}
@Bean
public AuthProvider authProvider() {
return new AuthProvider();
}
@Bean
public LoginUrlEntryPoint urlEntryPoint() {
return new LoginUrlEntryPoint("/user/login");
}
@Bean
public LoginAuthFailHandler authFailHandler() {
return new LoginAuthFailHandler(urlEntryPoint());
}
@Bean
public AuthenticationManager authenticationManager() {
AuthenticationManager authenticationManager = null;
try {
authenticationManager = super.authenticationManager();
} catch (Exception e) {
e.printStackTrace();
}
return authenticationManager;
}
@Bean
public AuthFilter authFilter() {
AuthFilter authFilter = new AuthFilter();
authFilter.setAuthenticationManager(authenticationManager());
authFilter.setAuthenticationFailureHandler(authFailHandler());
return authFilter;
}
}
解析:可以自定义哪些URL需要权限验证,哪些不需要。只需要在我们的SecurityConfig类中覆写configure(HttpSecurity http)方法即可。
方法解释:
http.authorizeRequests()方法有很多子方法,每个子匹配器将会按照声明的顺序起作用
指定用户可以访问的多个url模式。特别的,任何用户可以访问以"/static"开头的url资源
任何以"/admin"开头的请求限制用户具有 "ADMIN"角色。
任何以"/user"、“/api/user/”开头的请求限制用户具有 "ADMIN"或“USER”角色。*
源码解析:
默认的configure(HttpSecurity http)方法继续向httpSecurity类中追加SecurityConfigurer的具体实现类,如authorizeRequests()方法追加一个ExpressionUrlAuthorizationConfigurer,formLogin()方法追加一个FormLoginConfigurer。
其中ExpressionUrlAuthorizationConfigurer这个实现类比较重要,因为他会给我们创建一个非常重要的对象FilterSecurityInterceptor对象,FormLoginConfigurer对象比较简单,但是也会为我们提供一个在安全认证过程中经常用到会用的一个Filter:UsernamePasswordAuthenticationFilter。
添加授权过滤器
public class AuthFilter extends UsernamePasswordAuthenticationFilter {
@Autowired
private IUserService userService;
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String name = obtainUsername(request);
if(!Strings.isNullOrEmpty(name)){
request.setAttribute("username",name);
return super.attemptAuthentication(request, response);
}
User user = userService.findUserByUsername(name);
if(Objects.equals(user.getUsername,name)){
return new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities());
}else{
throw new BadCredentialsException("userCodeError");
}
}
}
解释:从request中取出authentication, authentication是在org.springframework.security.web.context.SecurityContextPersistenceFilter过滤器中通过捕获用户提交的登录表单中的内容生成的一个org.springframework.security.core.Authentication接口实例.
基于角色的登录入口控制器
public class LoginUrlEntryPoint extends LoginUrlAuthenticationEntryPoint {
private static final String API_FREFIX = "/api";
private static final String API_CODE_403 = "{\"code\": 403}";
private static final String CONTENT_TYPE = "application/json;charset=UTF-8";
private PathMatcher pathMatcher = new AntPathMatcher();
private final Map authEntryPointMap;
public LoginUrlEntryPoint(String loginFormUrl) {
super(loginFormUrl);
authEntryPointMap = new HashMap<>();
// 普通用户登录入口映射
authEntryPointMap.put("/user/**", "/user/login");
// 管理员登录入口映射
authEntryPointMap.put("/admin/**", "/admin/login");
}
/**
* 根据请求跳转到指定的页面,父类是默认使用loginFormUrl
* @param request
* @param response
* @param exception
* @return
*/
@Override
protected String determineUrlToUseForThisRequest(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) {
String uri = request.getRequestURI().replace(request.getContextPath(), "");
for (Map.Entry authEntry : this.authEntryPointMap.entrySet()) {
if (this.pathMatcher.match(authEntry.getKey(), uri)) {
return authEntry.getValue();
}
}
return super.determineUrlToUseForThisRequest(request, response, exception);
}
/**
* 如果是Api接口 返回json数据 否则按照一般流程处理
* @param request
* @param response
* @param authException
* @throws IOException
* @throws ServletException
*/
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
String uri = request.getRequestURI();
if (uri.startsWith(API_FREFIX)) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType(CONTENT_TYPE);
PrintWriter pw = response.getWriter();
pw.write(API_CODE_403);
pw.close();
} else {
super.commence(request, response, authException);
}
}
登录验证失败处理器
public class LoginAuthFailHandler extends SimpleUrlAuthenticationFailureHandler {
private final LoginUrlEntryPoint urlEntryPoint;
public LoginAuthFailHandler(LoginUrlEntryPoint urlEntryPoint) {
this.urlEntryPoint = urlEntryPoint;
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
String targetUrl =
this.urlEntryPoint.determineUrlToUseForThisRequest(request, response, exception);
targetUrl += "?" + exception.getMessage();
super.setDefaultFailureUrl(targetUrl);
super.onAuthenticationFailure(request, response, exception);
}
添加授权
public class AuthProvider implements AuthenticationProvider {
@Autowired
private IUserService userService;
private final Md5PasswordEncoder passwordEncoder = new Md5PasswordEncoder();
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String userName = authentication.getName();
String inputPassword = (String) authentication.getCredentials();
User user = userService.findUserByName(userName);
if (user == null) {
throw new AuthenticationCredentialsNotFoundException("authError");
}
if (this.passwordEncoder.isPasswordValid(user.getPassword(), inputPassword, user.getId())) {
return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
}
throw new BadCredentialsException("authError");
}
@Override
public boolean supports(Class> authentication) {
return true;
}
}
解析:
拿到authentication对象后,过滤器会调用ProviderManager类的authenticate方法,并传入该对象.
ProviderManager类的authenticate方法再调用自身的doAuthentication方法,在doAuthentication方法中会调用类中的List
由此可见,真正的验证逻辑是由各个各个AuthenticationProvider接口实现类来完成的,DaoAuthenticationProvider类是默认情况下注入的一个AuthenticationProvider接口实现类.