选择安全框架,Apache Shiro
已经提供了足够强大且灵活的权限管理功能,它的优势在于简单易于上手。
但若要结合Spring Cloud
微服务框架来使用,就需要考虑功能更多更全更容易与Spring Cloud
整合的Spring Security
了,毕竟Spring Cloud
似乎已经在主流框架中有着不可撼动的地位。
本项目采用
Spring Boot 2.1.3.RELEASE
&Spring Cloud Greenwich.RELEASE
搭建,并考虑了开放接口服务和第三方登录服务,因此从技术选型上来说,提供OAuth2 Server
和OAuth2 Client
模块的Spring Security
更加适合。所以将从Spring Security
开始下手,剖析Spring Cloud
系列源码。
阅读官方文档,能够发现Spring Security
提供的模块有点多:
模块 | 说明 |
---|---|
spring-security-remoting |
集成 Spring Remoting 的安全校验 |
spring-security-web |
提供网页安全校验,基于URL的访问控制 |
spring-security-ldap |
LDAP轻量目录访问协议功能模块 |
spring-security-oauth2-* |
用户资源授权功能模块 |
spring-security-acl |
访问控制列表功能模块 |
spring-security-cas |
单点登录功能模块 |
spring-security-openid |
openId功能模块 |
文中未列出代码的文件:
ApiResult
类参考:ApiResult.java
BasicErrorCode
类参考:BasicErrorCode.java
PasswordUtils
类参考:PasswordUtils.java
JsonUtils
类参考:JsonUtils.java
完整POM
文件参考:auth-pom.xml
部分来自Spring-Security官方文档
# 如果你的项目使用了Spring Boot
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
dependencies>
# 如果没有使用Spring Boot,可以考虑这样做
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-bomartifactId>
<version>5.1.4.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-configartifactId>
dependency>
dependencies>
在集成Spring Security
之前,先编写一些Controller
方法,和配置一些必要参数用于测试。
import df.zhang.BasePackage;
import df.zhang.base.pojo.ApiResult;
import df.zhang.util.JsonUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.*;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.bind.annotation.*;
/**
* 鉴权模块启动类。{@link BasePackage}为root包下的类文件,为各模块的Application指引包名路径。
*
* @author df.zhang Email: [email protected]
* @date 2019-04-21
* @since 1.0.0
*/
@SpringBootApplication(scanBasePackageClasses = BasePackage.class)
@Slf4j
@RestController
public class AuthApplication implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(AuthApplication.class, args);
}
/**
* 使用自定义的ObjectMapper将对象序列化为JSON字符串,参考{@link JsonUtils}
*
* @return org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
* @date 2019-05-04 03:21
* @author df.zhang
* @since 1.0.0
*/
@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
return new MappingJackson2HttpMessageConverter(JsonUtils.getObjectMapper());
}
@Override
public void run(String... args) {
log.info("My Cloud Authorization Running...");
}
/**
* 配置需要登录认证后访问的controller
*
* @return df.zhang.base.pojo.ApiResult<java.lang.String>
* @date 2019-05-04 03:22
* @author df.zhang
* @since 1.0.0
*/
@GetMapping("authenticated")
public ApiResult<String> testAuthenticated() {
return ApiResult.<String>success().res("authenticated");
}
/**
* 配置仅可以匿名访问的controller
*
* @return df.zhang.base.pojo.ApiResult<java.lang.String>
* @date 2019-05-04 03:22
* @author df.zhang
* @since 1.0.0
*/
@GetMapping("anonymous")
public ApiResult<String> testAnonymous() {
return ApiResult.<String>success().res("anonymous");
}
}
编写Java Configuration -- SecurityConfigurer
import org.springframework.context.annotation.*;
import org.springframework.security.config.annotation.web.configuration.*;
import org.springframework.security.core.userdetails.*;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
* Spring Security配置类
*
* @author df.zhang Email: [email protected]
* @date 2019-04-21
* @since 1.0.0
*/
@EnableWebSecurity
@Configuration
public class SecurityConfigurer extends WebSecurityConfigurerAdapter {
/**
* 这段代码描述为Spring-Security启动项目创建一个admin账户,角色为ADMIN。
* 该用户信息保存在内存中,项目停止时会被清除。
*
* @return org.springframework.security.core.userdetails.UserDetailsService
* @date 2019-05-02 16:12
* @author df.zhang
* @since 1.0.0
*/
@Override
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("admin").password("{noop}admin").roles("ADMIN").build());
return manager;
}
}
启动后访问服务地址,会跳出Spring自带的登录页面,输入账号密码即可登录成功。
在内存账号配置中能够看到密码使用了{noop}
作为前缀,这是一种类似明文密码的写法,但事实上调试在Dao身份认证提供器DaoAuthenticationProvider
时,类中第90行代码(也可以更前面)。
!passwordEncoder.matches(presentedPassword, userDetails.getPassword())
这行代码在校验密码正确性时,userDetails
对象中的password
属性值是{bcrypt}
开头的,当然这可以看作是Spring Security
的默认处理。
它的具体实现是org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
,BCrypt
这个算法很有趣,首先它足够安全,其次它足够慢,因此需要在安全和效率中间做个取舍,那就是使用够快且和别人的不一样的MD5 & SHA-1
。
Apache Commons Codec
来实现MD5 & SHA-1
算法。<dependency>
<groupId>commons-codecgroupId>
<artifactId>commons-codecartifactId>
dependency>
import org.apache.commons.codec.digest.DigestUtils;
import java.util.Objects;
/**
* 密码工具类
*
* @author df.zhang Email: [email protected]
* @date 2019-05-03
* @since 1.0.0
*/
public final class PasswordUtils {
/** 一袋盐*/
private static final byte[] SALT = DigestUtils.md5("@~df.zhang~@");
/** 交叉合并后byte数组的长度*/
private static final int ENCODE_LEN = 32;
/**
* 密码加密工具,使用{@link DigestUtils}将密码转换为16位长度的MD5字节数组,
* 然后与预先设置好的SALT数组交叉合并,得到最终32位长度的字节数组,转换为SHA-1字符串。
*
* @param rawPassword param1
* @return java.lang.String
* @date 2019-05-03 17:48
* @author df.zhang
* @since 1.0.0
*/
public static String encode(CharSequence rawPassword) {
assert Objects.nonNull(rawPassword);
byte[] rawBytes = DigestUtils.md5(rawPassword.toString());
byte[] encodeBytes = new byte[ENCODE_LEN];
// 将两个字节数组交叉组合成一个字节数组,循环中可实现某个数组反转
int jump = 2;
for (int i = 0, j = 0; i < ENCODE_LEN; i += jump, j++) {
encodeBytes[i] = rawBytes[j];
encodeBytes[i + 1] = SALT[j];
}
return DigestUtils.sha1Hex(encodeBytes);
}
}
在粗略的百万次测试中,大部分生成时间都在1500纳秒左右,也就是一秒钟可生成66万次,比网上资料所说直接MD5差一半,但至少要比0.3秒一次的BCrypt强很多。
SecurityConfigurer
中,将自定义的密码加密工具注册到Spring Ioc
容器,并重新配置内存账户。public class SecurityConfigurer extends WebSecurityConfigurerAdapter {
/**
* 使用自定义的密码加密工具
*
* @return org.springframework.security.crypto.password.PasswordEncoder
* @date 2019-05-02 17:23:25
* @author df.zhang
* @since 1.0.0
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new PasswordEncoder() {
@Override
public String encode(CharSequence rawPassword) {
return PasswordUtils.encode(rawPassword);
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encode(rawPassword).equals(encodedPassword);
}
};
}
/**
* 这段代码描述为Spring-Security启动项目创建一个admin账户,角色为ADMIN。
* 该用户信息保存在内存中,项目停止时会被清除。
* 使用密码加密工具后,内存账户的密码不能再直接配置为明文,需要进行加密。
*
* @return org.springframework.security.core.userdetails.UserDetailsService
* @date 2019-05-02 16:12
* @author df.zhang
* @since 1.0.0
*/
@Override
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("admin").password(PasswordUtils.encode("admin")).roles("ADMIN").build());
return manager;
}
}
UserDetailsService
新建用户状态枚举类UserStateEnum
。 非必须。
该枚举类描述用户当前状态,Spring Security
会根据用户登录时状态的不同,向用户响应不同的登录结果。
/**
* 用户状态枚举类
*
* @author df.zhang Email: [email protected]
* @date 2019-05-02
* @since 1.0.0
*/
public enum UserStateEnum {
/** 用户正常*/
ENABLED(0),
/** 用户已禁用*/
DISABLED(1),
/** 用户被锁定*/
LOCKED(2),
/** 用户已过期*/
EXPIRED(3),
/** 项目授权已过期*/
CREDENTIALS_EXPIRED(4);
private int state;
UserStateEnum(int state) {
this.state = state;
}
public int getState() {
return this.state;
}
static UserStateEnum[] VALUES = UserStateEnum.values();
public static UserStateEnum findByState(int state) {
for (UserStateEnum userStateEnum : VALUES) {
if (userStateEnum.state == state) {
return userStateEnum;
}
}
return ENABLED;
}
}
新建自定义的
UserDetails
,里面的所有属性最好只能在初始化时设置,即没有set方法。也是非必须
仍然可以使用User.withUsername(username).password(password).build();
的方式来构建
import df.zhang.auth.constant.UserStateEnum;
import org.springframework.security.core.*;
import java.util.Collection;
/**
* 自定义的{@link UserDetails}实现类,存放用户登录信息。
* 用户校验不是在{@link UserDetailsService}中完成,而是在各种provider中。
* 所以需要将用户名和密码存放进来,校验成功后会放入缓存(redis)。
*
* @author df.zhang Email: [email protected]
* @date 2019-05-02
* @since 1.0.0
*/
public class CustomUserDetails implements UserDetails {
private long userId;
private String username;
private String password;
private UserStateEnum state;
public CustomUserDetails(long userId, String username, String password, UserStateEnum state) {
this.userId = userId;
this.username = username;
this.password = password;
this.state = state;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
public long getUserId() {
return userId;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return state != UserStateEnum.EXPIRED;
}
@Override
public boolean isAccountNonLocked() {
return state != UserStateEnum.LOCKED;
}
@Override
public boolean isCredentialsNonExpired() {
return state != UserStateEnum.CREDENTIALS_EXPIRED;
}
@Override
public boolean isEnabled() {
return state == UserStateEnum.ENABLED;
}
}
新建UserDetailsService
实现类
UserDetailsService.loadUserByUsername(String username)
可以基于任意数据库实现。但必须要保证返回的UserDetails
中有鉴权需要的信息,如username
、password
(加密后),如果用户设计时有加入用户状态,也可将用户状态封装入UserDetails
。
import df.zhang.auth.constant.UserStateEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.userdetails.*;
/**
* 自定义的用户信息载入类,当输入用户名在平台数据库中不存在时,允许类中抛出异常{@link UsernameNotFoundException}
*
* @author df.zhang Email: [email protected]
* @date 2019-05-02
* @since 1.0.0
*/
@Slf4j
public class CustomUserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("用户名[{}]尝试登录。", username);
if (!"admin".equals(username)) {
throw new UsernameNotFoundException(username);
}
return new CustomUserDetails(1L, "admin", "7445b0991419189b5c3848d2195f3cb9f99c3a25", UserStateEnum.ENABLED);
}
}
调整SecurityConfigurer
中UserDetailsService
的配置
@Override
@Bean
public UserDetailsService userDetailsService() {
return new CustomUserDetailsServiceImpl();
}
可以尝试修改用户状态为不同类型的值,看看结果是什么样的。
到目前为止,登录都是使用Spring的默认接口(“/login
”)来实现,它提供了一个简单的登录页面和错误处理。但实际项目应用中,当前后端分离,当APP对接,这个默认接口就显得毫无意义。
重写SecurityConfigurer.configure(HttpSecurity http)
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 禁用匿名
.anonymous().disable()
// 配置所有请求都至少是登录用户才能访问
.authorizeRequests().anyRequest().authenticated()
// 配置表单登录,此处可以改登录路径
.and().formLogin()
// 配置成功处理类
.successHandler((request, response, authentication) -> {
ApiResult<CustomUserDetails> apiResult = ApiResult.<CustomUserDetails>success()
.res((CustomUserDetails) authentication.getPrincipal());
apiResult.setMsg("登录成功");
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().print(JsonUtils.serialize(apiResult));
response.getWriter().flush();
})
// 配置失败处理类
.failureHandler(((request, response, exception) -> {
ApiResult<String> apiResult = new ApiResult<String>()
.errorCode(BasicErrorCode.USERNAME_NOTFOUND);
apiResult.setMsg("用户名或密码错误");
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().print(JsonUtils.serialize(apiResult));
response.getWriter().flush();
}));
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
...
配置异常处理器
.and().exceptionHandling()
.authenticationEntryPoint((request, response, authException) -> {
ApiResult<String> apiResult = new ApiResult<String>().errorCode(BasicErrorCode.USER_NOT_LOGIN);
apiResult.setMsg("用户未登录");
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().print(JsonUtils.serialize(apiResult));
response.getWriter().flush();
})
.accessDeniedHandler((request, response, accessDeniedException) -> {
ApiResult<String> apiResult = new ApiResult<String>().errorCode(BasicErrorCode.USER_UNAUTHORIZED);
apiResult.setMsg("无权限访问");
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().print(JsonUtils.serialize(apiResult));
response.getWriter().flush();
});
}
创建单元测试基类
import df.zhang.auth.AuthApplication;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
/**
* 单元测试基类
*
* @author df.zhang Email: [email protected]
* @date 2019-05-02
* @since 1.0.0
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = AuthApplication.class)
public abstract class BaseTest {
protected MockMvc mockMvc;
@Autowired
private WebApplicationContext context;
@Before
public void setupMockMvc() {
mockMvc = MockMvcBuilders.webAppContextSetup(context).apply(SecurityMockMvcConfigurers.springSecurity()).build();
}
}
新建登录测试类
import df.zhang.test.BaseTest;
import org.junit.Test;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
/**
* 登录测试类
*
* @author df.zhang Email: [email protected]
* @date 2019-05-04
* @since 1.0.0
*/
public class LoginTest extends BaseTest {
protected MockHttpSession session;
@Test
public void test() throws Exception {
// 登录
MvcResult result = mockMvc.perform(SecurityMockMvcRequestBuilders.formLogin().user("admin").password("admin"))
.andReturn();
System.out.println(result.getResponse().getContentAsString());
session = (MockHttpSession) result.getRequest().getSession();
// 匿名访问
result = mockMvc.perform(MockMvcRequestBuilders.get("/anonymous")).andReturn();
System.out.println(result.getResponse().getContentAsString());
// 登录后请求仅匿名可访问的资源
result = mockMvc.perform(MockMvcRequestBuilders.get("/anonymous").session(session)).andReturn();
System.out.println(result.getResponse().getContentAsString());
// 未登录请求需登录后才能访问的资源
result = mockMvc.perform(MockMvcRequestBuilders.get("/authenticated")).andReturn();
System.out.println(result.getResponse().getContentAsString());
// 登录后请求登录后可访问的资源
result = mockMvc.perform(MockMvcRequestBuilders.get("/authenticated").session(session)).andReturn();
System.out.println(result.getResponse().getContentAsString());
}
}
测试结果:
// 登录
{"code":"10000","msg":"登录成功","res":{"user_id":1,"username":"admin","password":"7445b0991419189b5c3848d2195f3cb9f99c3a25","enabled":true,"credentials_non_expired":true,"account_non_locked":true,"account_non_expired":true}}
// 匿名访问
{"code":"10000","msg":"success","res":"anonymous"}
// 登录后请求仅匿名可访问的资源
{"code":"11100","msg":"无权限访问","err_code":"11102","err_msg":"unauthorized"}
// 未登录请求需登录后才能访问的资源
{"code":"11100","msg":"用户未登录","err_code":"11101","err_msg":"not_login"}
// 登录后请求登录后可访问的资源
{"code":"10000","msg":"success","res":"authenticated"}
简单总结
最开始配置时,Spring Security
默认提供了一套基于Form表单的配置,该配置为:
protected void configure(HttpSecurity http) throws Exception {
http.formLogin();
}
FormLoginConfigurer
;public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception {
return getOrApply(new FormLoginConfigurer<>());
}
FormLoginConfigurer
**中注册了用户名密码身份认证过滤器UsernamePasswordAuthenticationFilter
;public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
/login
”请求;UsernamePasswordAuthenticationToken
AuthenticationManager
实例的authenticate(Authentication authentication)
方法进行身份验证。具体的验证过程则由身份认证提供器AuthenticationProvider
负责。public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
}
}
}
一般来说
身份认证管理器AuthenticationManager
的实现类都会是提供器管理者**ProviderManager
,
身份认证提供器AuthenticationProvider
的实现类则是在配置PasswordEncoder
时提到过的Dao身份认证提供器DaoAuthenticationProvider
。
Dao身份认证提供器DaoAuthenticationProvider
默认从UserDetailsService
中获取登录的用户信息并用于身份校验,因为它是用户名密码身份认证过滤器UsernamePasswordAuthenticationFilter
指定的用户名密码登录认证提供者。
这些配置都是在formLogin()
之前完成的,若要深究其流程,就需要对Spring Security
有个系统的了解。
在HttpSecurity
这个类中,Spring
提供了多种访问认证方式,如下:
访问认证方式 | 配置名称 | 过滤器名称 | 身份认证端点 |
---|---|---|---|
anonymous() /匿名访问认证 |
AnonymousConfigurer |
AnonymousAuthenticationFilter |
AnonymousAuthenticationProvider |
httpBasic() /Authorization请求头认证 |
HttpBasicConfigurer |
BasicAuthenticationFilter |
- |
formLogin() /表单认证 |
FormLoginConfigurer |
UsernamePasswordAuthenticationFilter |
DaoAuthenticationProvider |
oauth2Login() /OAuth2认证 |
OAuth2LoginConfigurer |
OAuth2LoginAuthenticationFilter |
- |
匿名访问认证需要在WebSecurityConfigurerAdapter
中开启匿名访问,并指定哪些请求可以匿名访问。
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// 配置/anonymous开头的请求仅可匿名访问,登录后不可访问
.antMatchers("/anonymous/**").anonymous()
// 配置所有请求需登录访问
.anyRequest().authenticated()
.and().anonymous();
}
/**
* 配置Security需要忽略的访问路径,所有忽略的访问路径都不会再经过Security的任何Filter
*
* @param web {@link WebSecurity}
* @date 2019-05-04 16:14
* @author df.zhang
* @since 1.0.0
*/
@Override
public void configure(WebSecurity web) {
web.ignoring()
// 使用浏览器访问任意路径,都会向服务器拿取页面图标信息。此处将其忽略
.antMatchers("/favicon.ico");
}
需要注意的是配置
authorizeRequests()
时,anonymous()
和authenticated()
的顺序,若.anyRequest().authenticated()
在前,匿名访问配置.antMatchers("/anonymous/**").anonymous()
就不会生效。因为后一个会被前一个覆盖。
anonymous()
方法返回一个基于表达式的URL权限配置器ExpressionUrlAuthorizationConfigurer
,这是一个基于SPEL表达式
的URL访问权限拦截器配置类,后续会在过滤器安全拦截器FilterSecurityInterceptor
类中获取其具体的配置参数并传递给指定投票器来检查当前用户的URL访问权限。
过滤器安全拦截器FilterSecurityInterceptor
主要用于保证URL在进入过滤器链之前拦截用户无权限访问的资源。
(此处应有图)
AnonymousAuthenticationFilter
;AnonymousAuthenticationFilter
用于获取当前上下文SecurityContextHolder.getContext()
中的身份认证信息Authentication
,没有就新建一个匿名身份认证令牌AnonymousAuthenticationToken
public class AnonymousAuthenticationFilter extends GenericFilterBean implements InitializingBean {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
"获取当前线程下的身份认证信息"
if (SecurityContextHolder.getContext().getAuthentication() == null) {
"新建一个匿名身份认证令牌"
SecurityContextHolder.getContext().setAuthentication(createAuthentication((HttpServletRequest) req));
}
"继续执行过滤器链"
chain.doFilter(req, res);
}
"新建一个匿名身份认证令牌"
protected Authentication createAuthentication(HttpServletRequest request) {
AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key, principal, authorities);
auth.setDetails(authenticationDetailsSource.buildDetails(request));
return auth;
}
}
FilterSecurityInterceptor
封装过滤器参数传递对象FilterInvocation
对象并在FilterSecurityInterceptor.invoke(FilterInvocation fi)
方法中检查当前用户是否有当前路径的访问权限;
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
"封装过滤器参数传递对象"
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null) && observeOncePerRequest) {
...
} else {
...
"进入父类beforeInvocation方法"
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
}
}
FilterSecurityInterceptor.invoke(FilterInvocation fi)
方法的主要实现在抽象父类抽象的安全拦截器AbstractSecurityInterceptor
中,检查可访问权限的主要代码为第233行的this.accessDecisionManager.decide(authenticated, object, attributes)
。
当前URL是否可访问将由访问决策管理器AccessDecisionManager
进行选举,权限不通过会抛出AccessDeniedException
异常,并中断本次过滤器处理,直接返回异常结果。
public abstract class AbstractSecurityInterceptor implements InitializingBean,
ApplicationEventPublisherAware, MessageSourceAware {
protected InterceptorStatusToken beforeInvocation(Object object) {
...
"object为FilterInvocation对象,标明当前URL为:/anonymous,且能够取得对应的访问控制配置ant [pattern = /anonymous/**]"
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
if (attributes == null || attributes.isEmpty()) {
...
}
"取得当前身份认证信息"
Authentication authenticated = authenticateIfRequired();
try {
this.accessDecisionManager.decide(authenticated, object, attributes);
} catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, accessDeniedException));
throw accessDeniedException;
}
}
}
当然在此之前,Spring Security
还需要从项目的配置中找到与URL匹配的Web(SPEL)表达式配置属性WebExpressionConfigAttribute
,通过对Web表达式配置管理器方法FilterSecurityInterceptor.obtainSecurityMetadataSource()
的追溯,可以看到Web(SPEL)表达式配置属性WebExpressionConfigAttribute
是如何传递:
anonymous()
方法返回基于表达式的URL权限配置器ExpressionUrlAuthorizationConfigurer
,在完成配置时将anonymous
与所有请求匹配器RequestMatcher
(antMatchers("/anonymous/**")
是其中之一)添加到表达式拦截器URL注册表ExpressionInterceptUrlRegistry
实例的urlMappings
集合中;
URL权限配置器方法ExpressionUrlAuthorizationConfigurer.createMetadataSource(H http)
方法将所有urlMappings
写入到基于表达式的过滤器调用安全元数据中心ExpressionBasedFilterInvocationSecurityMetadataSource
实例中;
根据上一步骤,由于基于表达式的URL权限配置器ExpressionUrlAuthorizationConfigurer
继承自抽象的URL拦截配置器AbstractInterceptUrlConfigurer
,在调用AbstractInterceptUrlConfigurer.configure(H http)
创建过滤器FilterSecurityInterceptor
时,将SecurityMetadataSource
这个对象传递到了FilterSecurityInterceptor
实例中;
FilterSecurityInterceptor
在针对URL进行拦截时,会在SecurityMetadataSource
中查找与当前URL匹配的RequestMatcher
,若存在,就会进行下一步AccessDecisionManager.decide(authenticated, object, attributes)
(访问决策管理器)的检查。
根据这个流程,可以自定义一个URL权限拦截器。
AffirmativeBased
和WebExpressionVoter
检查当前用户是否有当前路径的访问权限;在AbstractSecurityInterceptor
(也就是FilterSecurityInterceptor
)中,accessDecisionManager
的具体实现为AffirmativeBased
,访问决策管理器**AccessDecisionManager
**接口有三个不同逻辑的实现,描述如下:
投票器实现自访问决策投票器AccessDecisionVoter
接口,其实现有抽象类AbstractAclVoter、AuthenticatedVoter、Jsr250Voter、PreInvocationAuthorizationAdviceVoter、RoleVoter、WebExpressionVoter
等。
WebExpressionVoter
用于检查在项目中配置的URL访问控制;投票器有三种结果判定
投票结果 | 说明 | 对应值 |
---|---|---|
ACCESS_GRANTED | 肯定票 | 1 |
ACCESS_ABSTAIN | 弃权票 | 0 |
ACCESS_DENIED | 否决票 | -1 |
AffirmativeBased
一票肯定管理器。
ConsensusBased
计分投票管理器,不计弃权票。
allowIfEqualGrantedDeniedDecisions
决策。UnanimousBased
一票否决管理器。
allowIfAllAbstainDecisions
决策。在WebExpressionVoter
源码第42行,Spring Security
将从第3步传递下来的Collection
中获取到WebExpressionConfigAttribute
对象,它将作为SPEL的表达式Expression。
而在第48行,Authentication
和FilterInvocation
将作为SPEL Contenxt的root。
public class WebExpressionVoter implements AccessDecisionVoter<FilterInvocation> {
private SecurityExpressionHandler<FilterInvocation> expressionHandler = new DefaultWebSecurityExpressionHandler();
"Authentication是当前匿名用户身份认证令牌
FilterInvocation是由第2步中FilterSecurityInterceptor向下传递的过滤器间调用数据封装类,URL为:/anonymous
Collection<ConfigAttribute>则是第3步中取得的WebExpressionConfigAttribute集合
该表达式用于SPEL,此时有一个元素为:authorizeExpression = anonymous"
public int vote(Authentication authentication, FilterInvocation fi, Collection<ConfigAttribute> attributes) {
...
WebExpressionConfigAttribute weca = findConfigAttribute(attributes);
"将authentication和fi封装成WebSecurityExpressionRoot对象
见DefaultWebSecurityExpressionHandler.createSecurityExpressionRoot()"
EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication, fi);
ctx = weca.postProcess(ctx, fi);
"已知 authorizeExpression = anonymous ctx.root = WebSecurityExpressionRoot
通过SPEL获取WebSecurityExpressionRoot实例中anonymous的值(bool)
见下文SecurityExpressionRoot"
return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED : ACCESS_DENIED;
}
}
Authentication
和FilterInvocation
两个对象最终会被封装成类型为WebSecurityExpressionRoot
的对象,其父类为SecurityExpressionRoot
,其中提供了一个is-getter方法isAnonymous()
。通过表达式[Expression = anonymous]可以得到,其值为true。
public abstract class SecurityExpressionRoot implements SecurityExpressionOperations {
public final boolean isAnonymous() {
return trustResolver.isAnonymous(authentication);
}
}