授权
认证
在授权之前是需要用户先通过认证的,鉴权的意思就是对项目的资源进行权限的控制,**项目中的代码方法不是所有的用户都可以访问的, 访问特定的方法需要特定的权限才可以访问。**授权在security中也是帮助我们封装了相应的逻辑,我们只是需要提供相应的数据给security 就行了, 然后在需要权限控制的代码方法中进行限定即可。
前提准备
- 用户
- 项目中的使用者
- 角色
- 使用者拥有的角色, 比如管理员等
- 权限值
- 和角色是相对应的,一种角色对应一种权限值
对应数据表
1、权限表
CREATE TABLE `t_permission` (
`id` int(32) NOT NULL,
`code` varchar(32) NOT NULL COMMENT '权限标识符',
`description` varchar(64) DEFAULT NULL COMMENT '描述',
`url` varchar(128) DEFAULT NULL COMMENT '请求地址',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2、角色表
CREATE TABLE `t_role` (
`id` varchar(32) NOT NULL,
`role_name` varchar(255) DEFAULT NULL,
`description` varchar(255) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`status` char(1) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `unique_role_name` (`role_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
3、用户表
CREATE TABLE `t_user` (
`id` bigint(20) NOT NULL COMMENT '用户id',
`username` varchar(64) NOT NULL,
`password` varchar(64) NOT NULL,
`fullname` varchar(255) NOT NULL COMMENT '用户姓名',
`mobile` varchar(11) DEFAULT NULL COMMENT '手机号',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
4 、用户角色表
CREATE TABLE `t_user_role` (
`user_id` varchar(32) NOT NULL,
`role_id` varchar(32) NOT NULL,
`create_time` datetime DEFAULT NULL,
`creator` varchar(255) DEFAULT NULL,
PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
5、 角色权限表
CREATE TABLE `t_role_permission` (
`role_id` varchar(32) NOT NULL,
`permission_id` varchar(32) NOT NULL,
PRIMARY KEY (`role_id`,`permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
主要过程
- 根据用户查出当前用户的角色信息
- 根据角色信息查出权限值
- 得到当前用户的所有权限值
- 按照规则授权
授权分析
授权
Security的授权过程可以理解成各种 filter 处理最终完成一个授权。 在项目初始化的时候会创建Filter链
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E5UqB8VQ-1597570804164)(E:\Code\my\spring_security\README.assets\1597563726543.png)]
spring security内置的各种filter:
Alias | Filter Class | Namespace Element or Attribute |
---|---|---|
CHANNEL_FILTER | ChannelProcessingFilter | http/intercept-url@requires-channel |
SECURITY_CONTEXT_FILTER | SecurityContextPersistenceFilter | http |
CONCURRENT_SESSION_FILTER | ConcurrentSessionFilter | session-management/concurrency-control |
HEADERS_FILTER | HeaderWriterFilter | http/headers |
CSRF_FILTER | CsrfFilter | http/csrf |
LOGOUT_FILTER | LogoutFilter | http/logout |
X509_FILTER | X509AuthenticationFilter | http/x509 |
PRE_AUTH_FILTER | AbstractPreAuthenticatedProcessingFilter Subclasses | N/A |
CAS_FILTER | CasAuthenticationFilter | N/A |
FORM_LOGIN_FILTER | UsernamePasswordAuthenticationFilter | http/form-login |
BASIC_AUTH_FILTER | BasicAuthenticationFilter | http/http-basic |
SERVLET_API_SUPPORT_FILTER | SecurityContextHolderAwareRequestFilter | http/@servlet-api-provision |
JAAS_API_SUPPORT_FILTER | JaasApiIntegrationFilter | http/@jaas-api-provision |
REMEMBER_ME_FILTER | RememberMeAuthenticationFilter | http/remember-me |
ANONYMOUS_FILTER | AnonymousAuthenticationFilter | http/anonymous |
SESSION_MANAGEMENT_FILTER | SessionManagementFilter | session-management |
EXCEPTION_TRANSLATION_FILTER | ExceptionTranslationFilter | http |
FILTER_SECURITY_INTERCEPTOR | FilterSecurityInterceptor | http |
SWITCH_USER_FILTER | SwitchUserFilter | N/A |
-
SecurityContextPersistenceFilter 在后续的Filter中可以SecurityContext 使用来获取信息
- 拦截我们的请求, 从session中获取SecurityContext 然后放到上下文中
- loadContext 方法 获得 SecurityContext
- 没有就会创建一个
-
FilterSecurityInterceptor
- 会从SecurityMetadataSource 的子类
DefaultFilterInvocationSecurityMetadataSource 获取要访问当前资源所需要的权限 - FilterSecurityInterceptor 会调用AccessDecisionManager 进行授权决策
- 会从SecurityMetadataSource 的子类
-
AccessDecisionManager 对于方法鉴权时候的投票机制
- AffirmativeBased
- ConsensusBased
- UnanimousBased
SecurityContextPersistenceFilter
SecurityContextPersistenceFilter 这个过滤器会拦截我们的请求, 首先在他它的构造方法中会传递过一个HttpSessionSecurityContextRepository , 它是管理ServletContent 的工具(存放security的全局信息), 得到这个管理工具之后
public SecurityContextPersistenceFilter() {
this(new HttpSessionSecurityContextRepository());
}
public SecurityContextPersistenceFilter(SecurityContextRepository repo) {
this.repo = repo;
}
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (request.getAttribute(FILTER_APPLIED) != null) {
// ensure that filter is only applied once per request
chain.doFilter(request, response);
return;
}
final boolean debug = logger.isDebugEnabled();
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
if (forceEagerSessionCreation) {
HttpSession session = request.getSession();
if (debug && session.isNew()) {
logger.debug("Eagerly created session: " + session.getId());
}
}
// 封装请求
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
response);
// 去 HttpSessionSecurityContextRepository 中获取 SecurityContext
SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
try {
SecurityContextHolder.setContext(contextBeforeChainExecution);
chain.doFilter(holder.getRequest(), holder.getResponse());
}
finally {
SecurityContext contextAfterChainExecution = SecurityContextHolder
.getContext();
// Crucial removal of SecurityContextHolder contents - do this before anything
// else.
SecurityContextHolder.clearContext();
repo.saveContext(contextAfterChainExecution, holder.getRequest(),
holder.getResponse());
request.removeAttribute(FILTER_APPLIED);
if (debug) {
logger.debug("SecurityContextHolder now cleared, as request processing completed");
}
}
}
repo.loadContext(holder);
HttpSessionSecurityContextRepository 获取全局的 SecurityContext, 会看到如果获取不到(用户没登录)就会创建一个。 readSecurityContextFromSession 方法中我们 可以看到是从session中取出名为SPRING_SECURITY_CONTEXT的attribute 可以看到,如果是之前登录过的,则session会关联上登录用户信息,包含用户的AuthenticationT信息,比如Principal,Granted Authorities等,这些都是重要的鉴权依据。
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
HttpServletRequest request = requestResponseHolder.getRequest();
HttpServletResponse response = requestResponseHolder.getResponse();
HttpSession httpSession = request.getSession(false);
SecurityContext context = readSecurityContextFromSession(httpSession);
if (context == null) {
if (logger.isDebugEnabled()) {
logger.debug("No SecurityContext was available from the HttpSession: "
+ httpSession + ". " + "A new one will be created.");
}
// 创建一个
/**
protected SecurityContext generateNewContext() {
return SecurityContextHolder.createEmptyContext();
}
**/
context = generateNewContext();
}
SaveToSessionResponseWrapper wrappedResponse = new SaveToSessionResponseWrapper(
response, request, httpSession != null, context);
requestResponseHolder.setResponse(wrappedResponse);
requestResponseHolder.setRequest(new SaveToSessionRequestWrapper(
request, wrappedResponse));
return context;
}
AnonymousAuthenticationFilter
AnonymousAuthenticationFilter 在上面获得不到 SecurityContext 的时候就会到这个过滤器中 Authentication的值就是AnonymousAuthenticationToken , 然后在登录成功之后就会把 认证的用户信息 authentication写入到SecurityContextHolder的context中。
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+ authResult);
}
// authentication写入到SecurityContextHolder的context中。
SecurityContextHolder.getContext().setAuthentication(authResult);
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
successHandler.onAuthenticationSuccess(request, response, authResult);
}
再次 SecurityContextPersistenceFilter
try {
SecurityContextHolder.setContext(contextBeforeChainExecution);
chain.doFilter(holder.getRequest(), holder.getResponse());
}
finally {
SecurityContext contextAfterChainExecution = SecurityContextHolder
.getContext();
// Crucial removal of SecurityContextHolder contents - do this before anything
// else.
SecurityContextHolder.clearContext();
repo.saveContext(contextAfterChainExecution, holder.getRequest(),
holder.getResponse());
request.removeAttribute(FILTER_APPLIED);
if (debug) {
logger.debug("SecurityContextHolder now cleared, as request processing completed");
}
}
可以看到SecurityContextPersistenceFilter在filter执行完之后,finally的时候,调用了saveContext来保存context的东西到session中。 SecurityContextPersistenceFilter通过往session存取名为SPRING_SECURITY_CONTEXT,值为SecurityContext的attribute,来为后续filter建立所需的上下文,包括登录态。
AccessDecisionManager
public interface AccessDecisionManager {
/**
* 通过传递的参数来决定用户是否有访问对应受保护资源的权限
*/
void decide(Authentication authentication , Object object, Collection<ConfigAttribute>
configAttributes ) throws AccessDeniedException, InsufficientAuthenticationException;
//略..
}
这里着重说明一下decide的参数:
- authentication:要访问资源的访问者的身份
- object:要访问的受保护资源,web请求对应FilterInvocation
- configAttributes:是受保护资源的访问策略,通过SecurityMetadataSource获取。
- decide接口就是用来鉴定当前用户是否有访问对应受保护资源的权限。
AccessDecisionManager 拥有三个不同的子类来实现具体的投票规则, 最终判断是否能够访问资源
授权时序图
测试
1、登录
- SecurityContextPersistenceFilter 拦截
- 可以看到contextBeforeChainExecution 是空的
2、加载用户信息
3、成功
4、访问接口, 显示的有当前用户的信息
- SecurityContextPersistenceFilter 拦截
- 可以看到contextBeforeChainExecution 显示的是当前用户的所有信息
- AffirmativeBased 拦截
- decide(Authentication authentication, Object object, Collection configAttributes) 投票
5、结果