Spring Security授权简介
授权又是什么概念呢?
现在来回顾下安全的概念:
- 你是谁?
- 你能干什么?
前面讲解的全是认证,也就是解决你是谁的问题;
这章讲解你能干什么的问题。很多人叫权限控制,鉴权,授权等;最终的核心目的都是一样的,
控制这个用户能在系统中干什么?
security对授权的定义
上图意思就是说,页面能看到的只是体验和ui交互问题;而对应后台某一个url的是否能被访问被认为是权限
权限场景分析
两个系统的权限特点是不一样的。不应该部署在一个应用中
当权限比较简单的时候,也就是对应业务系统来说这种需求;
security就支持了,可以把规则写在代码中进行控制
security的权限控制
之前其实已经写过
// 对请求授权配置:注意方法名的含义
.authorizeRequests()
.antMatchers(
SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,
SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE,
securityProperties.getBrowser().getLoginPage(),
SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX + "/*", // 图形验证码接口
securityProperties.getBrowser().getSignUpUrl(), // 注册页面
securityProperties.getBrowser().getSession().getSessionInvalidUrl() + ".json",
securityProperties.getBrowser().getSession().getSessionInvalidUrl() + ".html",
"/user/regist", // 注册请求,后面会介绍怎么把这个只有使用方知道放行的配置剥离处理
"/error",
"/connect/*",
"/auth/*",
"/signin"
)
// 放行以上路径
.permitAll()
// 该路径,只允许有 ADMIN 角色的人访问
.antMatchers("/user").hasRole("ADMIN")
.anyRequest()
// 对任意请求都必须是已认证才能访问
.authenticated()
这个时候再访问系统,登录后发现 /user 不能访问了;访问被拒绝了,那么就是因为当前登录的用户没有 ADMIN这个角色
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Sun Aug 12 17:23:47 CST 2018
There was an unexpected error (type=Forbidden, status=403).
Forbidden
如何给用户分配角色
就是在我们之前实现的UserDetailsService来进行构建的用户信息
com.example.demo.security.MyUserDetailsService
private SocialUser getUserDetails(String username) {
String password = passwordEncoder.encode("123456");
logger.info("数据库密码{}", password);
SocialUser admin = new SocialUser(username,
// "{noop}123456",
password,
true, true, true, true,
AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN"));
return admin;
}
这里的角色需要添加 ROLE 前缀;因为之前使用的是 hasRole ; 这个在下一章节讲解是为什么;
怎么匹配restfull的url
还是之前那个方法配置,可以传递请求和通配符
.antMatchers(HttpMethod.GET, "/user/*").hasRole("ADMIN")
在权限规则简单的情况下,就可以使用这里的知识进行构建权限系统。
Spring Security源码解析
spring security的基本原理之前讲解过了。这章主要看后面两个:
-
FilterSecurityInterceptor
决定该用户是否有权限访问指定的资源
-
ExceptionTranslationFilter
异常处理,如果不能访问,则处理FilterSecurityInterceptor中抛出的异常
AnonymousAuthenticationFilter
AnonymousAuthenticationFilter : 匿名过滤器,位置固定,前面所有的都走完之后,会经过该过滤器
该过滤器之前自己也是备受折磨,特别是在调试oath2的时候,在没有正确开始basic登录的时候,
就一直在抱怨,为什么会走这个呢?
看看最主要的源码:
public AnonymousAuthenticationFilter(String key) {
// 固定了用户信息和角色
this(key, "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
}
public AnonymousAuthenticationFilter(String key, Object principal,
List<GrantedAuthority> authorities) {
Assert.hasLength(key, "key cannot be null or empty");
Assert.notNull(principal, "Anonymous authentication principal must be set");
Assert.notNull(authorities, "Anonymous authorities must be set");
this.key = key;
this.principal = principal; // 用户信息不是一个对象,是一个字符串
this.authorities = authorities;
}
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
// 如果之前没有身份认证
// 则创建一个 AnonymousAuthenticationToken
// 也就是说到达 FilterSecurityInterceptor 中的时候一定有一个身份信息
if (SecurityContextHolder.getContext().getAuthentication() == null) {
SecurityContextHolder.getContext().setAuthentication(
createAuthentication((HttpServletRequest) req));
if (logger.isDebugEnabled()) {
logger.debug("Populated SecurityContextHolder with anonymous token: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'");
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("SecurityContextHolder not populated with anonymous token, as it already contained: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'");
}
}
chain.doFilter(req, res);
}
protected Authentication createAuthentication(HttpServletRequest request) {
AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key,
principal, authorities);
auth.setDetails(authenticationDetailsSource.buildDetails(request));
return auth;
}
FilterSecurityInterceptor
看源码技巧:找准一个关键入口,然后分析调用关系和类图
-
FilterSecurityInterceptor
-
AccessDecisionManager 管理什么呢?AccessDecisionVoter :投票者
就是用来一系列的判定,是否可以进行访问什么的
-
AffirmativeBased 只要有一票否决则不能访问 (默认使用)
在未登录的时候用的是只要有一个拒绝则不能访问,在登录后,只要有一个允许则放行。。有点懵逼
-
ConsensusBased 通过/不通过 哪一个票数多就遵循哪一个
-
UnanimousBased 只要有一票否决则不允许访问
-
-
AccessDecisionVoter 投票者 spring3-,由一堆实现类来解决
- WebExpressionVoter spring3+ 由该类全部包办,它投过就过,不过就不过
-
-------- 以上是三个核心的类---------
-
SecurityConfig 所有的配置信息
-
ConfigAttribute 对应了每一个url的配置信息
-
SecurityContextHolder
-
Authentication 用户身份信息,用户拥有的权限信息
主流程:拿到 配置信息,用户身份信息,请求信息 然后给投票者进行投票;最后根据策略进行决定是否放行
从两个流程进行分析:
- 未登录访问被拦截
- 登录后访问
未登录访问被拦截
访问: http://localhost:8080/user/1
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements
Filter {
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
// 拿到请求和响应封装成一个 invocation(调用)
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
public void invoke(FilterInvocation fi) throws IOException, ServletException {
// 保证每次请求只被检查一次,依据就是 request中的 FILTER_APPLIED
if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
// filter already applied to this request and user wants us to observe
// once-per-request handling, so don't re-do security checking
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {
// 如果是第一次经过该过滤器,则设置标记
// first time this request being called, so perform security checking
if (fi.getRequest() != null && observeOncePerRequest) {
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
// 权限的校验
InterceptorStatusToken token = super.beforeInvocation(fi);
// 如果校验通过则是执行真正的服务了
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
}
}
// 抽象父类
public abstract class AbstractSecurityInterceptor implements InitializingBean,
ApplicationEventPublisherAware, MessageSourceAware {
protected InterceptorStatusToken beforeInvocation(Object object) {
Assert.notNull(object, "Object was null");
final boolean debug = logger.isDebugEnabled();
if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
throw new IllegalArgumentException(
"Security invocation attempted for object "
+ object.getClass().getName()
+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "
+ getSecureObjectClass());
}
// 获取配置信息中对访问的url匹配的权限
// 这里会返回 hasRole('ROLE_ADMIN') ,也就是之前配置的admin角色
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
if (attributes == null || attributes.isEmpty()) {
if (rejectPublicInvocations) {
throw new IllegalArgumentException(
"Secure object invocation "
+ object
+ " was denied as public invocations are not allowed via this interceptor. "
+ "This indicates a configuration error because the "
+ "rejectPublicInvocations property is set to 'true'");
}
if (debug) {
logger.debug("Public object - authentication not attempted");
}
publishEvent(new PublicInvocationEvent(object));
return null; // no further work post-invocation
}
if (debug) {
logger.debug("Secure object: " + object + "; Attributes: " + attributes);
}
if (SecurityContextHolder.getContext().getAuthentication() == null) {
credentialsNotFound(messages.getMessage(
"AbstractSecurityInterceptor.authenticationNotFound",
"An Authentication object was not found in the SecurityContext"),
object, attributes);
}
// 获取身份信息
Authentication authenticated = authenticateIfRequired();
// Attempt authorization 尝试授权,也就是投票
try {
// accessDecisionManager 的实现是 AffirmativeBased,里面只有一个 WebExpressionVoter
// 如果没有通过就会抛出异常
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));
// 在这里异常会被 org.springframework.security.web.access.ExceptionTranslationFilter#doFilter 捕获到
throw accessDeniedException;
}
if (debug) {
logger.debug("Authorization successful");
}
if (publishAuthorizationSuccess) {
publishEvent(new AuthorizedEvent(object, attributes, authenticated));
}
// Attempt to run as a different user
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
attributes);
if (runAs == null) {
if (debug) {
logger.debug("RunAsManager did not change Authentication object");
}
// no further work post-invocation
return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
attributes, object);
}
else {
if (debug) {
logger.debug("Switching to RunAs Authentication: " + runAs);
}
SecurityContext origCtx = SecurityContextHolder.getContext();
SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
SecurityContextHolder.getContext().setAuthentication(runAs);
// need to revert to token.Authenticated post-invocation
return new InterceptorStatusToken(origCtx, true, attributes, object);
}
}
}
// 默认投票策略
public class AffirmativeBased extends AbstractAccessDecisionManager {
public AffirmativeBased(List<AccessDecisionVoter<? extends Object>> decisionVoters) {
super(decisionVoters);
}
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
int deny = 0;
for (AccessDecisionVoter voter : getDecisionVoters()) {
// WebExpressionVoter 去投票
int result = voter.vote(authentication, object, configAttributes);
if (logger.isDebugEnabled()) {
logger.debug("Voter: " + voter + ", returned: " + result);
}
switch (result) {
// 如果 是允许访问,则立即返回,也就是只要有一个同意则放行
case AccessDecisionVoter.ACCESS_GRANTED:
return;
// 如果返回的是拒绝则计数
case AccessDecisionVoter.ACCESS_DENIED:
deny++;
break;
default:
break;
}
}
// 如果没有允许访问,则判定是否是被拒绝了。
// 被拒绝则异常给调用者
if (deny > 0) {
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
// To get this far, every AccessDecisionVoter abstained
checkAllowIfAllAbstainDecisions();
}
}
public class ExceptionTranslationFilter extends GenericFilterBean {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
try {
chain.doFilter(request, response);
logger.debug("Chain processed normally");
}
catch (IOException ex) {
throw ex;
}
catch (Exception ex) {
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
// 获取到异常链中,第一次抛出的是否是身份异常信息
RuntimeException ase = (AuthenticationException) throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (ase == null) {
// 这一次流程是未登录访问
// 所以是被拒绝访问的异常
ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
AccessDeniedException.class, causeChain);
}
if (ase != null) {
// 检测响应是否已经提交
// 提交过得响应已经写了 http状态码和响应头
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
}
// 处理安全异常
handleSpringSecurityException(request, response, chain, ase);
}
else {
// Rethrow ServletExceptions and RuntimeExceptions as-is
if (ex instanceof ServletException) {
throw (ServletException) ex;
}
else if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
// Wrap other Exceptions. This shouldn't actually happen
// as we've already covered all the possibilities for doFilter
throw new RuntimeException(ex);
}
}
}
private void handleSpringSecurityException(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, RuntimeException exception)
throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
logger.debug(
"Authentication exception occurred; redirecting to authentication entry point",
exception);
sendStartAuthentication(request, response, chain,
(AuthenticationException) exception);
}
else if (exception instanceof AccessDeniedException) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
logger.debug(
"Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point",
exception);
// 如果是 Anonymous 身份信息,又是拒绝访问异常
// 则发送,并开始授权
sendStartAuthentication(
request,
response,
chain,
new InsufficientAuthenticationException(
messages.getMessage(
"ExceptionTranslationFilter.insufficientAuthentication",
"Full authentication is required to access this resource")));
}
else {
logger.debug(
"Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
exception);
accessDeniedHandler.handle(request, response,
(AccessDeniedException) exception);
}
}
}
protected void sendStartAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
SecurityContextHolder.getContext().setAuthentication(null);
requestCache.saveRequest(request, response);
logger.debug("Calling Authentication entry point.");
// org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint#commence
// 该类是我们之前配置的 loginFormUrl = /authentication/require 的处理器端点
// 该端点会获取到 之前配置的地址然后使用
// redirectStrategy.sendRedirect(request, response, redirectUrl); 进行跳转
// 该访问之后就完成了拒绝并引导授权的功能
authenticationEntryPoint.commence(request, response, reason);
}
}
登录后访问
该源码就不记录了。篇幅太多了;
权限表达式
看源码得知,最后都会转成一个表达式,然后进行投票评估;
那么有哪些表达式呢?
这些表达式的由来,由代码中的配置而来。
.antMatchers().xxx 每个函数都包装了一个表达式生成。
跟着源码得到 返回的是一个 ExpressionUrlAuthorizationConfigurer.AuthorizedUrl 对象
联合使用是通过access方法,自己写表达式
.antMatchers("xx").access("hasRole('ROLE_USER') and hasRole('ROLE_SUPER')")
那么能自定义表达式,并且使用自己的代码逻辑来判定吗?是可以的,下一节讲解;
分离配置
如下配置,一部分是安全模块的配置,一部分是使用安全模块的应用自己的业务配置;
那么怎么能把这种业务配置分离出去呢?
.antMatchers(
SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,
SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE,
"/user/regist", // 注册请求,后面会介绍怎么把这个只有使用方知道放行的配置剥离处理
// org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController
// BasicErrorController 类提供的默认错误信息处理服务
"/error",
"/connect/*",
"/auth/*",
"/signin"
)
.permitAll()
// 该路径,只允许有 ADMIN 角色的人访问
.antMatchers(HttpMethod.GET, "/user/*").hasRole("ADMIN")
思路:
- 提供AuthorizeConfigProvider接口
- 权限模块的通用配置实现该接口,然后进行配置
- 其他的应用或则模块配置可以自己实现
- 最后使用 AuthorizeConfigManager类来管理所有的AuthorizeConfigProvider实现
- 拿到所有的配置后,进行统一设置
接口定义
package cn.mrcode.imooc.springsecurity.securitycore.authorize;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
/**
* 权限自定义配置管理
* @author : zhuqiang
* @version : V1.0
* @date : 2018/8/12 21:09
*/
public interface AuthorizeConfigManager {
void config(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config);
}
package cn.mrcode.imooc.springsecurity.securitycore.authorize;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
/**
* 自定义权限控制接口
* @author : zhuqiang
* @version : V1.0
* @date : 2018/8/12 21:09
*/
public interface AuthorizeConfigProvider {
/**
* @param config
* @see HttpSecurity#authorizeRequests()
*/
void config(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config);
}
核心实现: 通用配置的抽取,只是把app和browser中用到的配置都抽到公用的里面了
package cn.mrcode.imooc.springsecurity.securitycore.authorize;
import cn.mrcode.imooc.springsecurity.securitycore.properties.SecurityConstants;
import cn.mrcode.imooc.springsecurity.securitycore.properties.SecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.stereotype.Component;
/**
* app和browser通用静态权限配置
* @author : zhuqiang
* @version : V1.0
* @date : 2018/8/12 21:12
*/
@Component
public class CommonAuthorizeConfigProvider implements AuthorizeConfigProvider {
@Autowired
private SecurityProperties securityProperties;
@Override
public void config(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config) {
config.antMatchers(
SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,
SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE,
SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_OPEN_ID,
SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX + "/*",
securityProperties.getBrowser().getLoginPage(),
securityProperties.getBrowser().getSignUpUrl(),
securityProperties.getBrowser().getSession().getSessionInvalidUrl() + ".json",
securityProperties.getBrowser().getSession().getSessionInvalidUrl() + ".html"
).permitAll();
// 退出成功处理,没有默认值,所以需要判定下
String signOutUrl = securityProperties.getBrowser().getSignOutUrl();
if (signOutUrl != null) {
config.antMatchers(signOutUrl).permitAll();
}
}
}
package cn.mrcode.imooc.springsecurity.securitycore.authorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.stereotype.Component;
import java.util.Set;
/**
* @author : zhuqiang
* @version : V1.0
* @date : 2018/8/12 21:21
*/
@Component
public class DefaultAuthorizeConfigManager implements AuthorizeConfigManager {
@Autowired
private Set<AuthorizeConfigProvider> providers;
@Override
public void config(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config) {
for (AuthorizeConfigProvider provider : providers) {
provider.config(config);
}
// 除了上面配置的,其他的都需要登录后才能访问
config.anyRequest().authenticated();
}
}
浏览器中的安全配置:
// 有三个configure的方法,这里使用http参数的
@Override
protected void configure(HttpSecurity http) throws Exception {
applyPasswordAuthenticationConfig(http);
SessionProperties session = securityProperties.getBrowser().getSession();
http
.apply(validateCodeSecurityConfig)
.and()
.apply(smsCodeAuthenticationSecurityConfigs)
.and()
.apply(imoocSocialSecurityConfig)
.and()
.rememberMe()
.tokenRepository(persistentTokenRepository)
.tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())
.userDetailsService(userDetailsService)
.and()
.sessionManagement()
.invalidSessionStrategy(invalidSessionStrategy)
.maximumSessions(session.getMaximumSessions())
.maxSessionsPreventsLogin(session.isMaxSessionsPreventsLogin())
.expiredSessionStrategy(sessionInformationExpiredStrategy)
.and()
.and()
.logout()
.logoutSuccessHandler(logoutSuccessHandler)
.deleteCookies("JSESSIONID")
.and()
.csrf()
.disable();
// 注入进来,然后把调用下配置对象即可
// 可以看到上面的配置都没有了http.authorizeRequests()的配置
// 全部由具体的去实现配置了
// app项目中的安全配置改动其实和这里一样
authorizeConfigManager.config(http.authorizeRequests());
}
demo项目的安全配置
package com.example.demo.security;
import cn.mrcode.imooc.springsecurity.securitycore.authorize.AuthorizeConfigProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.stereotype.Component;
/**
* @author : zhuqiang
* @version : V1.0
* @date : 2018/8/12 21:25
*/
@Component
public class DemoAuthorizeConfigProvider implements AuthorizeConfigProvider {
@Override
public void config(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config) {
config.antMatchers(
"/user/regist", // 注册请求
// org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController
// BasicErrorController 类提供的默认错误信息处理服务
"/error",
"/connect/*",
"/auth/*",
"/signin",
"/social/signUp", // app注册跳转服务
"/swagger-ui.html",
"/swagger-ui.html/**",
"/webjars/**",
"/swagger-resources/**",
"/v2/**"
)
.permitAll()
// 这里配置了一个不存在的角色。
// 可以访问下 看是否有效果
.antMatchers("/user/*").hasRole("xxx")
;
}
}
基于数据库Rbac数据模型控制权限
前面都是讲的怎么在权限规则基本不变的情况下,怎么写代码控制权限;
这一节要实现内管系统的场景;
这些所有的信息都必须存在数据库中。因为变动频繁,员工离职、部门调动,新增权限等;
通用RBAC数据模型
Role-Based-Access Control
通常由三直系表,两张关系表
对于资源表:存储数据的表现是 某一个url的别名是菜单或则按钮;所以url和多个菜单或则按钮绑定;
这样业务人员分配权限的时候才能看得懂
虽然是多对多,但是可以根据业务需要,进行一对多,比如一个用户只能拥有一个角色;
这个数据模型可以解决ui的显示问题和后台的程序权限控制
ui显示问题:提供一个查询该用户所有资源信息,然后由前端进行控制哪些资源显示或隐藏
url的程序控制:由security来处理
数据库中的数据如何交由security?
// 任意请求都必须走自定义的表达式
// 该表达式的含义也很简单:rbacService 在容器中的beanName名称,后面的是方法名和参数名
config.anyRequest().access("@rbacService.hasPermission(request,authentication)")
只要有了入口,那么来实现这个功能;
新建一个项目,用来写这个表达式的功能;然后demo项目引用和配置表达式
security-authorize 依赖
dependencies {
compile 'javax.servlet:javax.servlet-api'
// 核心的类都在该包中
compile 'org.springframework.security:spring-security-core'
}
接口
package cn.mrcode.imooc.springsecurity.securityauthorize;
import org.springframework.security.core.Authentication;
import javax.servlet.http.HttpServletRequest;
public interface RbacService {
boolean hasPermission(HttpServletRequest request, Authentication authentication);
}
实现
package cn.mrcode.imooc.springsecurity.securityauthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import javax.servlet.http.HttpServletRequest;
import java.util.HashSet;
import java.util.Set;
@Component("rbacService")
public class RbacServiceImpl implements RbacService {
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
Object principal = authentication.getPrincipal();
boolean hasPermission = false;
// 有可能 principal 是一个Anonymous
// 所以只要是一个UserDetails那么就能标识是经过了我们自己的数据库查询的
// 当前需要先配置UserDetailsServices
if (principal instanceof UserDetails) {
String username = ((UserDetails) principal).getUsername();
//读取用户所拥有权限的所有URL
Set<String> urls = new HashSet<>();
for (String url : urls) {
if (antPathMatcher.match(url, request.getRequestURI())) {
hasPermission = true;
break;
}
}
}
return hasPermission;
}
}
demo项目中引用该包;并配置
package com.example.demo.security;
import cn.mrcode.imooc.springsecurity.securitycore.authorize.AuthorizeConfigProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.stereotype.Component;
/**
* @author : zhuqiang
* @version : V1.0
* @date : 2018/8/12 21:25
*/
@Component
public class DemoAuthorizeConfigProvider implements AuthorizeConfigProvider {
@Override
public void config(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config) {
config.antMatchers(
"/user/regist", // 注册请求
// org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController
// BasicErrorController 类提供的默认错误信息处理服务
"/error",
"/connect/*",
"/auth/*",
"/signin",
"/social/signUp", // app注册跳转服务
"/swagger-ui.html",
"/swagger-ui.html/**",
"/webjars/**",
"/swagger-resources/**",
"/v2/**"
)
.permitAll();
// 使用自定义的
config.anyRequest().access("@rbacService.hasPermission(request,authentication)");
}
}
注意:要把该包扫描到
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@SpringBootApplication(scanBasePackages =
{
"com.example.demo",
"cn.mrcode.imooc.springsecurity.securitybrowser",
"cn.mrcode.imooc.springsecurity.securityapp",
"cn.mrcode.imooc.springsecurity.securitycore",
"cn.mrcode.imooc.springsecurity.securityauthorize" // 注意添加扫描包
})
//@SpringBootApplication
@RestController
@EnableSwagger2
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@GetMapping("/hello")
public String hello() {
return "hello spring security";
}
}
ok。完成
其他问题
现在有两个问题需要解决:
- 在通用的AuthorizeConfigProvider里面有一个
config.anyRequest()
return requestMatchers(ANY_REQUEST); 的源码,显示只能配置一个
config.anyRequest()
的配置也只能在所有的配置完成之后,进行调用
第一个问题
先注释掉,通用AuthorizeConfigProvider里面的;
那么所有需要登录的配置要怎么办呢?视频中说后面总结的时候讲解;这里先留坑
第二个问题
使用order注解来解决,让我们的业务配置在最后;
第一次长见识,order还能用来控制 依赖查找的顺序
@Component
public class DefaultAuthorizeConfigManager implements AuthorizeConfigManager {
// 由于需要有序的,所以不能再使用set了
// 依赖查找技巧
@Autowired
private List<AuthorizeConfigProvider> providers;
@Component
@Order(Integer.MIN_VALUE)
public class CommonAuthorizeConfigProvider implements AuthorizeConfigProvider {
@Component
@Order(Integer.MAX_VALUE)
public class DemoAuthorizeConfigProvider implements AuthorizeConfigProvider {
本文由 liyunfei 创作,采用 知识共享署名4.0
国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: Jul 25,2022